C++学习:4拷贝友元
Posted 想文艺一点的程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:4拷贝友元相关的知识,希望对你有一定的参考价值。
目录
拷贝构造
1、拷贝构造函数
也是一种构造函数,只是比较特殊
问题:
什么时候调用构造函数? 创建一个对象的时候进行调用。
什么时候调用拷贝构造函数? 当利用已存在的对象创建一个新对象时(类似于复制粘贴,生成一个新文件)
- 拷贝构造函数的格式是固定的,接收一个const引用作为参数
理解拷贝:类似于复制粘贴,生成一个新文件, 所以我们希望 新文件和旧文件的内容是一样的。
- 当我们去掉拷贝构造函数,看看会发生什么。
- 当我们写了拷贝构造函数,但是里面是空的,会发生什么。
结果:并不会对成员变量进行拷贝。
- 在拷贝构造函数当中,添加拷贝代码。
反问:这情况下,我们写拷贝构造函数有用吗?
- 如果我们想拷贝所有成员变量,这样写根本没有必要。
- 因为不写,默认也会拷贝所有的成员变量。
我们有个性化需求的时候,可以写:
- 只希望部分成员变量进行拷贝操作。
2、调用父类的拷贝构造函数
怎么调用父类的拷贝构造函数?
- 如果我们是想要拷贝全部成员变量,那么就没有必要调用父类的拷贝构造函数。
Student s1(18,90);
Student s2(s2); // 这样会默认拷贝所有变量
注意点:
拷贝构造函数: 拷贝 + 构造 的时候被调用:
- 拷贝:赋值操作
- 构造:产生一个新的对象
注意点:
- 在初始化列表当中,通过调用父类构造函数,进行初始化。
- 同理:子类拷贝构造函数,也应该调用父类拷贝构造函数
- 可以发现是:父类指针指向子类对象
如果子类拷贝构造函数没有调用父类的拷贝构造函数,就只能给 m_score 赋值,不能给 m_age 进行赋值。
1、可以发现 m_score 并没有赋值,但是 m_age 变为了 0,而不是 0xcc。
2、因为 子类的构造函数会默认调用 父类的无参构造函数。 同理 子类的析构函数 也会默认调用 父类无参的析构函数。
3、 Person (int age = 0) : 因为有默认参数,所以不传入参数也可以,所以也算是无参的构造函数。
3、浅拷贝
编译器默认的提供的拷贝是浅拷贝(shallow copy) , 我们分析浅拷贝是为了更好的分析深拷贝。
回顾一下C语言当中的字符串,(字符串的本质其实就是字符数组)
const char *name = "bwm"; // 本质是一个常量
char name2[] = {'b','w','m'};
当我们类当中有一个指针变量的时候:然后调用构造函数
分析对象的内存模型
- 栈空间指向堆空间:Car *c1 = new Car(100,name);
- 堆空间指向栈空间:Car.name = name
注意:堆空间指向栈空间非常危险!!!
- 因为栈空间的释放,是系统自动回收内存的,我们无法控制。
- 当栈空间被回收的时候,而我们堆空间仍然会指向它,那么就非常危险。(堆空间的指针为野指针)
怎么解决:堆空间指向栈空间的问题?
让堆空间指向堆空间:
步骤:
1、先取消初始化列表的指针初始化,让将其放到构造函数当中初始化。
2、进行栈空间到堆空间的拷贝
3、当 c1 释放的时候,我们也要释放 m_name 的堆空间。
这时候就体现出 析构函数的作用了:
当 delete(c1) 的时候,自动调用 Car 的析构函数,我们将 delete(m_name)放到析构函数当中,就可以达到我们想要的目的。
变量图:
name:指向栈空间
m_name :指向堆空间
第二种情况:
前面都是铺垫,我们现在来分析浅拷贝。
浅拷贝:不用写拷贝构造函数,直接进行拷贝。
分析浅拷贝的特点:
- 将一个对象中所有成员变量的值拷贝到另一个对象。
造成问题一:指针也是直接进行拷贝, c2 和 c1 共用一个堆空间。(当 c1 的名字改变的时候,c2 的名字也会被改变)
造成的问题2:多次 free。可能我们看起来多次 free 好像没有什么问题,但是有些平台,多次 free 程序就会崩掉。
4、深拷贝
解决办法:使用深拷贝,即自己写一个拷贝构造函数,从而产生一个新的内存空间。
优化:
5、总结拷贝构造函数
回过头来看:我们什么时候需要自己写一个 拷贝构造函数呢?
情况1:如果class 当中都是 int 、double 这些简单的数据类型。
我们就没必要自己写拷贝构造函数,直接进行浅拷贝也不会出错。
情况2:class 当中我们如果含有 指针类型
- 浅拷贝:会造成堆空间的多次 free,以及多个对象共用一个堆空间。
这时候我们才真正需要自己去实现一个拷贝构造函数。
一些细节
1、对象参数和返回值
使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象 。
这个就是传值传递。
改进:我们利用传址传递。(传递一个地址就可以达到两种效果)
- 输出型参数:const
- 输出性参数:非const
对象作为返回值:多了一个 对象的创建
分析为什么?
- test 函数结束之后,car对象就会销毁,而我们还想将 car 拿到 main 函数里面用。
- 所以就会将 test函数当中 car,拷贝到 mian函数的栈空间。
改进:编译器进行优化,只进行了一次拷贝构造。
2、匿名对象、临时对象
匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构
匿名对象作为参数,传给一个函数:
3、隐式构造
C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数
可以知道,隐式构造有时候是不好的,可以通过关键字explicit禁止掉隐式构造
我再来体会一下构造函数:
修改:
分析为什么可以这么操作:
其实 Car c1 = 20 等价于 Car c1(20); 它默认调用了 单参数的构造函数
接着看:
作为返回值:
将关键字explicit 添加在构造函数前面, 因为这样的写法,代码可读性比较差。
4、编译器自动生成的构造函数
一个错误的广为传之的观点:编译器会为每一个类生成一个无参的空的构造函数。
直接分析汇编:
如果没有呢?
总结:只有特定情况下,才会生成 空的无参的构造函数。
反过来思考,编译器为什么会生成 空的无参的构造函数? 有什么用呢? 哪些情况下会生成 空的无参的构造函数?
1、成员变量在声明的同时进行了初始化
2、有定义虚函数 ,会对对象有什么影响呢?
- 有虚函数,对象前面的前 4 个字节就必须存放虚表地址。
3、虚继承了其他类,虚继承对对象有什么影响呢?
- 对象的前四个字节,又会有一个虚表地址。
4、包含对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 因为生成 Car 对象,就会生成 Person 对象。
- 生成给 Person 对象的时候就会调用 Person 类的构造函数。
- 所以会给 Car 生成一个构造函数,并且在其中调用 Person 类的构造函数。
5、父类有构造函数(编译器生成或自定义)
- 同理,继承的时候,在 子类的构造函数当中会默认调用父类的构造函数。
- 所以必须 先得给子类生成一个构造函数。
总结一下:
对象创建后,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数。
友元
首先明确友元分为两部分:友元函数、友元类。
典型的封装:
缺点:假设 add 的函数调用非常频繁,那么开销就会非常大,每调用一次add ,就会调用 4 次api接口。
改进:
- 要求:我们不能破坏原来的封装性。
- Point 的成员变量,必须还是 private, 保持原来的封装性。
办法:友元函数
1、友元函数
- 如果将函数 add(非成员函数)声明为类 point 的友元函数,那么函数 add 就能直接访问类point 对象的所有成员
2、友元类
如果将类A声明为类C的友元类,那么类A的所有成员函数都能直接访问类C对象的所有成员
设定友元类:
如果将类 Math 声明为类 Point 的友元类,那么类Math 的所有成员函数,都能直接访问类Point对象的所有成员。
总结:
友元破坏了面向对象的封装性,但在某些频繁访问成员变量的地方可以提高性能
3、内部类
含义:如果将类A定义在类C的内部,那么类A就是一个内部类(嵌套类)
特点:
1、支持public、 protected、 private权限, 可以控制这个类的访问权限。
2、成员函数可以直接访问其外部类对象的所有成员(反过来则不行)
3、成员函数可以直接不带类名、对象名访问其外部类的static成员
4、不会影响外部类的内存布局
- 仅仅是访问权限变了。
5、内部类 – 声明和实现分离
4、局部类
在一个函数内部定义的类,称为局部类
局部类的特性:
-
作用域仅限于所在的函数内部
-
其所有的成员必须定义在类内部,即声明和实现必须放到类里面。
-
不允许定义static成员变量,因为 static成员变量 必须放到类外初始化,与上面矛盾。
-
成员函数不能直接访问函数的局部变量(static变量除外)
以上是关于C++学习:4拷贝友元的主要内容,如果未能解决你的问题,请参考以下文章
C++入门不能重载为友元函数的4个运算符(=, ->, [ ], ( ))