经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
C++面向对象
来源:cnblogs  作者:老贝uu  时间:2024/6/25 8:51:36  对本文有异议

1. C++语言基础

1.1 函数

C++新增:多态

  • 函数重载( overload )

  • 函数重写(覆写,overrride)

编译器会根据实参的类型来?动确定调?哪个重载函数

C++新增:内联函数

修饰关键字:inline

作用:编译时直接将函数替换为一堆代码,减少函数调用带来的开销。

比#define安全

成员函数默认内联,即使不写inline;外部函数必须加inline才能内联。

内存分配管理

每一个函数在栈空间上都有一段栈帧,保存这当前函数所需的变量等。当函数出栈时,这些成员也随之销毁。

1.2 const修饰符

const修饰的对象、变量,在运行过程中不能修改其值。

  1. const int a = 1;
  2. a = 2; // 错误!
  3. // 会报错:error: assignment of read-only variable 'a'

注意:常量必须在声明时被初始化 const int a; // 错误!

关于const可修改的练习:

例1

  1. int main() {
  2. const int a = 1;
  3. int* add = &a; // 这一句有语法问题:invalid conversion from 'const int*' to 'int*'
  4. *add = 2;
  5. return 0;
  6. }

编译器在做隐式转换的时候不会添加或删除const修饰,如果类型不匹配会报语法错误。

正确做法:使用强制类型转换,消除语法问题

  1. int main() {
  2. const int a = 1;
  3. int* add = (int*)&a; // 正确
  4. *add = 2;
  5. return 0;
  6. }

编译器不会自作主张修改const修饰,因为可能有潜在风险,但是如果人为地强制转化,说明是程序员告诉编译器:“就这么干,我说的!“。编译器就会很放心地让程序员来承担责任,不会报错 /doge

注意:这里a为局部变量,即使被const修饰,也保存在栈上(假设没有常量折叠)。如果换做全局变量,被const修饰后存储在常量区中,为只读属性,没有办法修改的。

例2:指针变量也是变量,所以可用const修饰

常量指针

  1. int main() {
  2. char array[] = {'a', 'b', 'c', 'd', '\0'};
  3. char* const name = array; // 注意这里的 * 和 const的前后位置
  4. name[0] = 'H'; // 正确
  5. name = (char*)"Hi"; // 错误! assignment of read-only variable 'name'
  6. return 0;
  7. }

指向常量的指针

  1. int main() {
  2. char array[] = {'a', 'b', 'c', 'd', '\0'};
  3. char const* name = array; // 注意这里的 * 和 const的前后位置
  4. name[0] = 'H'; // 错误!
  5. name = (char*)"Hi"; // 正确
  6. return 0;
  7. }

这里可以这样子去记忆:const负责在他左边的东西,如果左边是char*,说明地址是常量,如果左边是char,说明字符是常量。

1.3 动态内存分配

常规的静态分配内存:int a[10] = {0};

数组a在编译的时候就被分配了固定大小的内存。

  1. int n = 10;
  2. int b[x]; // 对于多数编译器是不允许的,即使允许也会有?险,如多线程编程

而使用动态定义变量更灵活

  1. int n = 10;
  2. int* b = new int[n]();

注意:最后记得delete

1.4 作用域运算符::

  • 使用类的静态成员

  • 解除被隐藏全局函数或变量

    1. int num = 1; // 全局变量
    2. main() {
    3. int num = 2; // 局部变量
    4. cout << num << endl; // 显示局部变量
    5. cout << ::num << endl; // 显示全局变量
    6. }

2. 类和对象

注意点:

  • (C++11之前)在类体中不允许对数据成员进行初始化

    1. class A {
    2. int a = 0; // 在一些老版本的编译环境上会出错
    3. };
  • 类中的成员不能是自身类的对象

    否则会导致无限递归,最终栈溢出,因此会报编译错误。

    但是可以放自身类的指针或引用。

  • 若类A中含有成员类B,则类B需要提前声明

  • 数据成员不能用auto、register、extern修饰;成员函数不能用extern修饰

2.1 拷贝构造函数

注意点:

  • 形参必须是对象的引用,&不能省略,否则会向上文所述的无限递归。

  • 编译构造优化

  • 要特别注意浅拷贝的问题

    举个例子

    1. class A {
    2. char* name;
    3. ~A() { delete[] name; }
    4. };

    当调用拷贝构造函数的时候,编译器会无脑复制一份数据到新对象,此时两个对象中name指向同一个地址,当两个类调用析构函数时,name会被delete两次,第二次会出错。

    这种情况必须自己写拷贝构造函数。

  • 对象作为参数传递时,用引用或者指针好一点,可以避免一次拷贝构造,减少开销

2.2 静态成员

作用:为了解决同类对象间的数据共享问题,实际就是共享变量

静态成员在整个内存中只有一份,位于静态区。

在编译器遇到初始化语句时,就为其分配空间。

两种初始化方法:

  • 在类内初始化:必须加static修饰符
  • 在类内声明,要加static;在类外初始化,不加static

静态成员函数只能访问静态成员,因为非静态成员有对象才存在;相反,非静态成员函数可任意访问静态、非静态成员。在?般的成员函数中都隐含有?个this指针,?来指向对象自身,而在静态成员函数中没有this指针,这是无法访问非静态成员的技术原因。但是若向静态函数中传入对象,则可以访问对象中的非静态成员。

2.3 友元

允许?个类授权另?个类的对象(友元类)、某个成员函数(友元成员)或外部函数(友元函数)访问其对象的非公有成员,而不允许整个程序访问。是类的封装性与执行效率的折中。

Java保留了静态成员,舍弃了友元机制。

2.4 对象嵌套

比如类A中有成员变量类B,则在实例化A的时候,先调用B的构造函数,再调用A的构造函数(先里后外)

另外,在A的构造函数中,推荐使用构造函数初始化列表,来初始化B。

3. 派生类

重定义(隐藏)redefining:派生类中重新定义基类成员,包括数据成员、成员函数,可参考
重写(覆盖)overload:是重定义,但使用虚函数,实现了多态
在java中两者无区别

? 派生类重新定义了print,则派生类中有两个print,但隐藏了继承来的print——用派生类对象a调用print,即a.print(),调用的是重新定义的,除非a.person::print();或者用基类指针指向a,用指针调用print。如果是重写,则根据虚表直接找到重写的print。

3.1 派生方式

这里比较绕,分三步说明

1)权限变化

  • 公有派生中,权限不变。公有 -> 公有,私有 -> 私有,保护 -> 保护
  • 私有派生中,all -> 私有

2)类内访问权限

  • 基类的公有和保护成员都能够被派生类的成员访问

  • 基类的私有成员不能被派生类成员直接访问,但可以通过基类的公有成员函数间接访问,比如基类的get、set

3)类外访问权限

? 指的是实例化派生类后,能否通过a.print()的方式访问类内成员

? 从类外看,派生类和基类是融为一体的,就当作一个普通的类操作,所以当然是私有不能访问,公有可以访问,而具体是私有还是公有见 1)中分析

综上所述,派生方式只改变类外访问权限

3.2 派生类构造、析构函数

派生类不继承构造函数,因为不符合构造方法的命名规则。但调用派生类构造函数时,编译器也要调用基类构造函数,这不是真正意义上的继承。析构函数也是类似。
但基类析构函数可以在子类中用基类名显式调用

注意:C++基类的析构函数一般定义为虚函数,防止内存泄漏

执行顺序

创建派生类对象:
(1)先执行基类的构造函数
(2)执行派生类的构造函数

销毁派生类对象时,相反

如何传参

  • 如果基类的构造函数没有参数,或者没有显式地定义构造函数的时候,派生类构造函数不必考虑向基类构造函数传递参数,甚至可以不定义构造函数
  • 当基类中带有参数的构造函数的时候,派生类必须定义构造函数,且使用初始化列表传递参数

3.3 重新定义

3.4 多重继承

派生类有多个基类,派生方式可以各不相同

构造顺序

  1. A 基类
  2. / B C
  3. \ /
  4. D
  5. 构造顺序为 ABACD

但是多继承可能有模糊性(菱形继承的二义性) 。此外,从不同类继承同名成员,也会出现歧义性

3.5 虚拟继承

  1. class 派生类名 : virtual 派生方式 基类名{
  2. // .....
  3. };

编译器保证从虚基类中继承的成员只有一个拷贝,避免模糊性

一些语法规定

  1. A 基类
  2. / B C BC继承A时需要虚继承
  3. \ /
  4. D D不需要虚继承

也就是说,虚继承可以作用于一分为二的继承,避免基类成员也一分为二。而在合二为一的情况下,虚继承没有用

? 在D的构造函数中,需要用参数化列表调用ABC的构造函数,即在直接继承BC基础上,加一个虚基类(祖先)的构造函数。因此,BC在继承A的时候,必须是公有继承,不然D无法调用A的构造函数,因此虚继承必须是公有继承。

构造顺序

上图调用顺序为ABCD,虚基类(祖先)第一个调用,并且只会调用一次,从而保证其成员只会被初始化一次。当然,如果虚基类还有父类,那应该按正常流程先调用父类,再子类。然后,虚继承的类先构造,非虚继承的类后构造

即:

  1. 虚基类或非虚基类的祖先的构造函数
  2. 任何虚继承基类的构造函数按照它们被继承的顺序构造
  3. 任何非虚继承基类的构造函数按照它们被继承的顺序构造
  4. 对象成员的构造函数
  5. 派生类自己的构造函数

析构顺序相反

构造顺序 例1

  1. A
  2. |
  3. B (C) D (E)
  4. \ | | /
  5. F
  6. 其中F虚继承于CE,用括号表示,并包含一个M类成员
  7. class F : public B, virtual public C, public D, virtual public E {
  8. // ...
  9. M mem1;
  10. }

则构造顺序为:CEABDMF

构造顺序 例2

  1. class A {
  2. public:
  3. int a;
  4. A() { a = 10; }
  5. };
  6. class A1 : virtual public A {
  7. public:
  8. A1() { a++; }
  9. };
  10. class A2 : virtual public A {
  11. public:
  12. A2() { a++; }
  13. };
  14. class B : public A1, public A2 {
  15. public:
  16. B() { a++; }
  17. void print() { cout << a << endl; }
  18. };
  19. int main() {
  20. B b;
  21. b.print();
  22. return 0;
  23. }

结果输出:13

补充

虚基表

C++对象模型详解(整理)

图说C++对象模型:对象内存布局详解

C++:30 --- C++类成员,成员函数的内存布局

虚继承和虚基类的对象模型

大部分编译器都提供了查看C++代码中类内存分布的工具,比如vs。参考

  1. 虚基类的直接子类(Veichle_Road、Veichle_Water)存放指向虚基类表的指针,以及自己新定义成员。
  2. 后续子类(Amphicar)继承了两个父类的成员,包括虚基类表指针,并存放了虚基类成员(weight)的一个拷贝。

4. 多态性(polymorphism)

4.1 编译多态和运行多态

  • 重载(Overload):不同函数使用同一个函数名,即同样的接口实现不同的操作
  • 重写(Override):对虚函数的重新定义,即有继承关系的 不同类对象 对同一消息作出不同响应
  • 重新定义(Redefining):在派生类中定义一个新的
  • 运算符重载
  • 模板

联编

  • 静态联编:在编译阶段进行联编,速度快、效率高,但不灵活
  • 动态联编:在程序运行阶段进行联编,即直到程序运行时在确定调用哪个函数

在C++中:
函数重载(含运算符重载、重新定义、模版)是通过静态联编实现的
虚函数多态性(重写)是通过动态联编来实现的,使用虚函数表在运行时实现

举个例子

用来体会一下静态联编

  1. class Point {
  2. public:
  3. Point(double i, double j) {
  4. x = i;
  5. y = j;
  6. }
  7. double Area() const { return 0.0; } //
  8. private:
  9. double x, y;
  10. };
  11. class Rectangle : public Point {
  12. public:
  13. Rectangle(double i, double j, double k, double l);
  14. double Area() const { return w * h; } // 在派生类中重新定义
  15. private:
  16. double w, h;
  17. };
  18. Rectangle::Rectangle(double i, double j, double k, double l) : Point(i, j) {
  19. w = k;
  20. h = l;
  21. }
  22. void fun(Point& s) { // 外部函数
  23. cout << s.Area() << endl;
  24. }
  25. void main() {
  26. Rectangle rec(3.0, 5.2, 15.0, 25.0);
  27. fun(rec);
  28. rec.Area();
  29. }

注意fun函数的形参为Point&,即基类引用,因此会窄化,fun始终调用基类的Area()。若参数改为:Rectangle &s,也同样只能调用Rectangle中的Area()

4.2 函数重载

不同函数使用同一个函数名,由编译器来选择具体由哪个函数来执行

4.3 运算符重载

一般用于操作类,比如两个自定义类的 “+” 运算

显然,重载的运算符函数必须要能访问类中元素,因此运算符重载 要么是成员函数,要么是外部友元函数。如果用成员函数,那形参只有一个,如果是友元函数,形参有两个。特别的,( )[ ]->=,必须用成员函数实现运算符重载,<<(输出)必须用友元函数实现运算符重载。

有一些运算符是不能重载的,共5个:. .* :: ?:(三目运算符) sizeof

对于[]、<<,都是两目,但区别是第一个参数是本类对象或其它类对象,所以采用不同实现方法,原因:

  • 如果用成员函数实现,类的this指针会被绑定到运算符的左侧对象,防止出现异常的写法。这是与this的背后实现有关:比如a.f(),编译器先用this指向对象a,然后调用函数f,函数中访问数据成员时,编译器通过this到对象中找。对于a[i]的重载,[]是函数,所以仍遵循上述原理。

  • 如果是友元,则没有this指针,双目运算符左右对象无限制,比如第一形参是整型(下标),第二形参是对象。可能导致:3[a] 。可参考

  • 每个非静态成员函数都有一个隐式参数(编译器编译器编译时自动添加的),这个参数就是成员函数中this指针的来源,并且是第一个参数。
    比如:void A::f() {} ,实际上是:void A::f(A *const this) {} 编译器在编译时会自动传递参数,使得成员函数可以访问对象的数据成员。因为数据成员分配在对象中,而成员函数本类对象共用,分配在别的地方。
    << 运算符重载函数要求第一个参数必须是std::ostream & ,所以必须是非成员函数。但不一定非得是友元,除非访问私有成员

关于编译优化

值返回的时候会有所区别

  1. Integer operator+(const Integer& a) { // 情况1:返回局部对象
  2. return Integer(val + a.val);
  3. }
  4. Integer operator+(const int x) { // 情况2:返回局部对象
  5. Integer tmp;
  6. tmp.val += x;
  7. return tmp;
  8. }
  9. Integer operator+(const char x) { // 情况3:用*this返回
  10. return *this;
  11. }
  12. Integer operator-(const Integer x) { // 情况4:返回形参对象
  13. return x;
  14. }

在上述12两种返回局部对象的情况时,按理来说,应该是在return时创建一个匿名对象,再调用拷贝构造函数将其赋值给调用运算符的左值。而编译器会对此优化,不需要拷贝构造。

因此,上述四种情况分别会

  1. 一次普通构造
  2. 一次普通构造
  3. 一次拷贝构造
  4. 两次拷贝构造 (形参到实参也要一次)

增量运算符的重载

  1. Integer& operator++( ); //前缀增量运算符
  2. Integer operator++(int); //后缀增量运算符

? 前缀与后缀在调用时其实形式一样,为了在调用时能够区分前、后缀,c++后来(开始时前后缀无区分)规定:后缀函数加一个没有实际意义的int参数,这样编译器在判断出前后缀时,就知道调用哪一个了。在调用后缀函数时,自动传给参数int的值是0,所以该参数的变量名都可以省略

? 前缀按语义可连接,后缀不允许。此外后缀要先返回后加加,故返回对象。非要返回引用当然也可以,但会造成连续加加,不符合语义。

类型转换运算符重载

通过构造函数进行(简单类型 -> 类对象)

通过类型转换运算符重载进行(类对象-> 简单类型)

operator int();

  1. Integer::operator int() {
  2. cout << Type changed to int << endl;
  3. return value; // 没返回类型,但要返回
  4. }

注意:没有返回类型、参数

  • 没有返回类型,因为强制转换为int说明int就是返回类型,再写一遍多此一举
  • 没有参数,因为强制转换为单目运算符,加之用成员函数实现

赋值运算符重载

如果不重载,编译器会默认一个,其默认行为是复制对象的数据成员,就和默认拷贝构造函数一样,但这会导致浅拷贝

赋值运算符重载不能继承。实际是可继承,但因为派生类没定义赋值重载,则编译器默认一个,隐藏了继承来的

4.4 虚函数

动态联编实现的运行时多态

通常用基类指针(或引用)指向公有派生类对象。必须是公有派生,如果私有派生的话,从基类继承的成员都会私有,再拿基类指针指,首先会窄化,结果什么都访问不到。因此如果用基类指针指向私有、保护派生类对象,会直接在编译的时候报错

虚函数定义

基类中被关键字virtua修饰,派生类中virtual可省略

这里的virtual和前文的虚继承一点关系都没有,同名纯属巧合

实现原理

? 一个类有虚函数,则会定义虚函数表,本类只有一个。本类对象中都有虚函数表指针。虚函数表是函数指针数组,指向每个虚函数。

? 派生类继承了虚函数,则也继承了虚函数表(复制了虚函数表)。如果重写虚函数,则修改虚表中函数指针,否则仍指向基类虚函数。

? 编译阶段创建对象及类的虚表。运行时用基类指针访问到派生类对象,找到其虚表指针,查表找到虚函数执行。所以编译时只绑定了指针(包括对象中的虚函数表指针、虚函数表中的函数指针),运行时根据指针才能绑定虚函数。
? 所以无论基类指针指向基类对象还是派生类对象,能做到不同对象对相同消息做出不同响应。
? 当基类指针指向派生类对象时,编译器以为是基类对象,但因为是虚函数,所以编译器建立虚函数表并在对象中存放虚函数指针,导致运行时调用重写的虚函数。

? 对于p->vfunc1(),因为p是基类指针,则编译器已经将所指派生类对象隐式转为基类对象,所以无法判定调用哪个函数

注意事项

  • 不能把静态成员函数声明为虚函数,因为静态成员函数不属于某个对象,而虚函数是通过对象指针或引用来调用的
  • 构造函数不能是虚函数,因为执行构造函数时,对象还没有实例化。但析构函数可以是虚函数,且有时是必须的

? 针对第二点析构的,打个比方A *p = new B;,new了一个派生类对象,但是用的基类指针,也就是说最后调用了基类的析构,会导致派生类中新定义的成员无法销毁。因此析构函数也必须重写。

? 每个类的析构函数负责销毁自己新定义的成员,派生类只需要销毁掉新定义的即可,至于继承来的成员,会调用基类的析构销毁。整个流程就是:基类指针调用析构(虚函数),派生类析构,由于继承关系,基类析构(这里和虚函数没关系,纯属是“继承”的特性)

? 析构的虚函数有其特殊性,比如在基类和派生类中函数名不同,但就运行时多态而言,与其他虚函数性质一样。

? 详细研究虚析构函数在虚函数表情况,参见:c++虚析构函数在虚函数表中吗?

纯虚函数和抽象类

纯虚函数是一个在基类中说明的虚函数,它在该基类中仅定义了函数首部,必须在派生类中重写

定义:virtual 返回类型 函数名(参数表)=0;

有纯虚函数的类称为抽象类,其无法被实例化,不能做函数参数、返回类型或转换类型,但抽象类指针、引用可以

5. 模板

分为函数模板和类模板,是实现代码重用机制的一种工具

5.1 函数模板与模板函数

使用函数模板时需要实例化,可以显式、隐式实例化

  1. template <typename T>
  2. void swap(T &a, T &b){...}
  3. //隐式
  4. int main(){
  5. int x=1,y=2;
  6. swap<int>(x,y);
  7. //或swap(x,y);
  8. ....
  9. }
  10. //显式
  11. template
  12. void swap(int &a,int &b);
  13. int main(){
  14. int x=1,y=2;
  15. swap(x,y);
  16. ....
  17. }

函数模板中没有隐式的类型转换(形参到实参),但是重载后的模板函数(也就成了普通函数)是可以隐式转换的

  1. template <class T>
  2. T max(T x, T y) {
  3. return (x > y) ? x : y;
  4. }
  5. void func(int i, char c) {
  6. max(i, i); // 正确 调用max(int,int)
  7. max(c, c); // 正确 调用max(char,char)
  8. max(i, c); // 错误 类型不匹配
  9. max(c, i); // 错误 类型不匹配
  10. }

解决方法:

  • 采用强制类型转换,如将上述max(i,c);改写为max(i,int(c));

  • 用非模板函数去重载函数模板

    1. char max(int x, char y) {
    2. return (x > y) ? x : y;
    3. }

    这样就会调用这个普通的重载函数

模板函数、函数重载的匹配次序

优先级为:

  1. 参数完全匹配的(重载或实例化)函数
  2. (隐式)实例化后能匹配的模板函数
  3. 类型转换后能匹配上的重载函数

例如

  1. template <class T>
  2. T myMax(T x, T y) { // 模板
  3. // ...
  4. }
  5. int myMax(int x, int y) { // 重载1
  6. // ...
  7. }
  8. char myMax(int x, char y) { // 重载2
  9. // ...
  10. }
  11. int main() {
  12. int i = 10;
  13. char c = a’;
  14. float f = 43.74;
  15. cout << myMax(i, i) << endl; // 重载1
  16. cout << myMax(c, c) << endl; // 模板
  17. cout << myMax(i, c) << endl; // 重载2
  18. cout << myMax(c, i) << endl; // 重载2 (隐式转换)
  19. cout << myMax(f, f) << endl; // 模板
  20. cout << myMax(f, i) << endl; // 隐式转换是必须的,但是既可以转成重载1,又可以转成重载2
  21. // 此时选择哪一个由编译器决定,不同编译器可能有不同选择
  22. return 0;
  23. }

5.2 类模板和模板类

dddd

6. 代码巩固

运算符重载

有一个String类,只有一个成员变量

  1. class String {
  2. char* array;
  3. }

1)重载[]

这里为了理解面向对象的一些点,采用了很奇怪的写法,仅仅保证语法无误

关注点:返回值应该是引用

  1. char& operator[](int i) {
  2. if (i < 0 || i >= strlen(array)) {
  3. cout << "Index out of range." << endl;
  4. static char dummy = '\0';
  5. return dummy;
  6. }
  7. return array[i];
  8. }

2)重载+

如果要做String + String的运算

这是一种返回临时对象的写法

  1. String operator+(const String& s) { // 形参中的const和引用都可省略,语法上没问题
  2. String temp(array);
  3. temp.array = new char[strlen(array) + strlen(s.array) + 1];
  4. strcpy(temp.array, array);
  5. strcat(temp.array, s.array);
  6. return temp;
  7. }

如果要做String + char*的运算

  • 一种方法是重载+运算符,接收一个char*类型的参数

  • 一种方法是使用类型转换

    • 比如把char*转为String,用构造函数就可以实现,运算时编译器会自动隐式转换,从而转为String + String

      需要注意的是,这种方法会把char*转为一个临时对象String,这个临时对象作为+函数参数传入时,函数形参必须声明为const。也就是说,运算符重载函数接收一个临时对象时,必须是const。

    • 若把String转为char*,则需要重载char*()

    但是这两种方法不能同时使用,即不能同时有char* -> StringString -> char*,否则会有二义性:既可以转为char* + char*,又可以转为String + String

提供隐式类型转换

整个思路是:A重载了+运算符,但是只能和A相加,因此这里给B提供了一个转化为A的类型转换重载,编译器在计算a+b时,会自动把b隐式转换为a

  1. class A {
  2. int a;
  3. public:
  4. A() { a = 0; }
  5. A(int i) { a = i; }
  6. int operator+(const A& aa) { return a + aa.a; }
  7. };
  8. class B {
  9. int b;
  10. public:
  11. B() { b = 0; }
  12. operator A() { return A(b); }
  13. };
  14. int main() {
  15. A a;
  16. B b;
  17. cout << a + b << endl;
  18. return 0;
  19. }

输出运算符重载

如正文所讲,<<只能采用友元函数重载,并且返回值得是引用,第一个参数得是引用,第二个参数中如果要接收一个临时对象,必须用const修饰

  1. friend ostream& operator<<(ostream& out, const String& s) {
  2. out << s.array;
  3. return out;
  4. }

编译器的移动构造优化

  1. class A {
  2. int x;
  3. public:
  4. A(int i = 0) { // 构造
  5. x = i;
  6. cout << "construct";
  7. }
  8. A(const A& a) { // 拷贝构造
  9. cout << "copy";
  10. }
  11. A operator+(const A& a) { // 重载+
  12. cout << "add";
  13. return A(x + a.x);
  14. }
  15. void operator=(const A& a) { // 重载=
  16. cout << "equals";
  17. }
  18. };
  19. int main() {
  20. A a, b;
  21. A c = a;
  22. A d = a + b;
  23. return 0;
  24. }

这段代码最终输出为:con con copy add con

分析:

  • A a, b; 两次默认构造
  • A c = a; 标准的拷贝构造
  • A d = a + b; 显然先有一次 + 重载,之后编译器会优化,效果等价于 A d(a + b); 也就是一次普通构造

怎么区分 = 是拷贝构造还是赋值运算呢?只有在初始化对象的时候,才会使用拷贝构造,如果对象已经被创建了,之后的=都是赋值

小综合

题目:

有四种几何图形:三角形、矩形、正方形和圆。
求四种几何图形的面积之和。

定义一个包含两个虚函数的类

  1. class Shape {
  2. public:
  3. virtual float Area(void) = 0; // 求面积
  4. virtual void SetData(float, float = 0) = 0; // 设置几何形状参数
  5. // 不同集合形状需要1个或2个参数,故采用默认参数值
  6. };

公有派生各类,重写两个函数,这里只展示一个类,其余同理。

  1. class Triangle : public Shape {
  2. float W, H; // 三角形边长为W,高为H
  3. public:
  4. Triangle(float w = 0, float h = 0) {
  5. W = w;
  6. H = h;
  7. }
  8. float Area(void) { // 重写虚函数
  9. return W * H / 2;
  10. }
  11. void Setdata(float w, float h = 0) { // 重写虚函数
  12. W = w;
  13. H = h;
  14. }
  15. };

这里使用了一个指针数组,来实现多态;如果采用对象数组,则无法实现多态

  1. class Compute {
  2. Shape** s; // 指向基类的指针数组
  3. public:
  4. Compute() { // 给几何图形设置参数
  5. s = new Shape*[4];
  6. s[0] = new Triangle(3, 4);
  7. s[1] = new Rectangle(6, 8);
  8. s[2] = new Square(6.5);
  9. s[3] = new Circle(5.5);
  10. }
  11. float SumArea(void);
  12. ~Compute();
  13. void Setdata(int n, float a, float b = 0) { s[n]->Setdata(a, b); }
  14. };
  15. float Compute::SumArea(void) {
  16. float sum = 0;
  17. for (int i = 0; i < 4; i++)
  18. sum += s[i]->Area(); // 通过基类指针实现多态性
  19. return sum;
  20. }
  21. Compute::~Compute() {
  22. for (int i = 0; i < 4; i++)
  23. delete s[i];
  24. delete[] s;
  25. }

原文链接:https://www.cnblogs.com/laobei-uu/p/18265426

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号