目录
再谈构造函数
构造函数体赋值
初始化列表
explicit关键字
匿名对象
static成员
静态成员变量
静态成员函数
static相关习题
友元
内部类
习题 下面代码共有多少拷贝构造
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值.
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
#include
using namespace std; class Time { public: Time(int hour) { _hour = hour; } private: int _hour; }; class Date { public: Date(int year, int month, int day) { Time a(day); _t = a; _year = year; _month = month; _day = day; } private: Time _t; int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date a(1,1,1); return 0; } 当Time没有初始化列表,而我们相对其私有成员初始化时,可以用一个值进行接收,但是这样写程序执行后会报错,因为Time没有合适的默认构造函数
程序提供默认构造后可以执行
那么Time程序要是一直没有默认构造,该如何修改呢?此时可以用初始化列表
举例:自定义类型成员(且该类没有默认构造函数时)
#include
using namespace std; class Time { public: Time(int hour) { _hour = hour; } private: int _hour; }; class Date { public: Date(int year, int month, int day) :_t(day) { Time a(day); _t = a; _year = year; _month = month; _day = day; } private: Time _t; int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date a(1,1,1); return 0; } 用初始化列表,可以解决这一问题
俩种初始化方式的比较
第一种方式当程序走到左边这里的时候,再跳到Time的默认构造函数,这是因为程序没有找到用户写的Date默认构造函数,而是调用了系统默认生成的默认构造函数,对内置类型不处理,然后直接调用自定义类型的默认构造函数
所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中.
当程序走到这里时,要对b调用默认构造函数
对于Time而言调用了俩次默认构造函数,一次是在调用Date默认构造函数时,还有一次是在对b调用默认构造函数,这种定义一个变量再赋值给另一个变量的写法比较麻烦
如果用初始化列表写就不用这么麻烦,会直接调用初始化列表进行初始化
对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?由下面的测试可知,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
结论:自定义类型成员,推荐用初始化列表初始化
初始化列表可以认为是成员变量定义的地方,在定义的地方会调用他的构造
这种在函数体内赋值,但是还是会走初始化列表(虽然啥都没写但是有初始化列表),然后在他定义的地方调用默认构造
这是日期类对象整体的定义,但是对象里面还有成员,C++通过初始化列表去认定成员的定义
const成员变量也必须在初始化列表进行初始化
对const成员没初始化直接编不过,因为const修饰的对象在定义的时候就要初始化好,如果没有初始化,后面也不能对其赋值,而初始化列表就是定义的地方,所以要在初始化列表里面进行初始化,初始化列表使用逗号不用;
我们直接在main函数里定义一个引用变量,执行程序会报错,因为定义的时候没有引用,
在类里面声明引用,执行程序,也报错
引用的定义在初始化列表,但我们在初始化列表没有对其进行初始化,所以会报错
using namespace std; class Time { public: Time(int hour) { _hour = hour; } private: int _hour; }; class Date { public: Date(int year, int month, int day,int x) :_t(day) , _N(100) ,a(x) { _year = year; _month = month; _day = day; } private: Time _t; int _year; // 年 int _month; // 月 int _day; // 日 const int _N; int& a; }; int main() { int x = 0; Date a(1,1,1,x); return 0; }
此时程序正常运行
x2是x1的一份临时拷贝,给引用重新赋值只能改变x2,不能改变x1
如果想通过改变x2来改变x1,传参的时候再加一个引用即可
内置类型也推荐使用初始化列表,当然内置类型在函数体内初始化也没什么问题,但有些时候也需要用到函数体内初始化,初始化列表并不是万能的
观察下列程序运行结果
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private:声明 int _a2; int _a1; }; int main() { A aa(1); aa.Print(); }
初始化列表的初始化顺序是按照声明的顺序来初始化的,这里先声明了a2,再声明了a1
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
d1和d2的写法一样, d1是直接调用构造,d2是隐式类型转换:构造+拷贝构造+优化=直接调用构造
d2用2022构造一个Date的临时对象,再对临时对象拷贝构造,但编译器一般都会优化这俩步
并没有打印拷贝构造,这是因为发生了优化
加上关键字之后显示错误
用explicit修饰构造函数,将会禁止构造函数的隐式转换
所有的隐式类型转换中间都会产生一个临时变量,而这个临时变量具有常属性
去掉explicit关键字后如果这样写,引用的是中间产生的临时变量
红框的最终效果和蓝框的效果一样,但是红框比较麻烦,直接用隐式类型转换就行
传参的时候红框要拷贝构造,蓝框直接调用构造,蓝框的直接构造含有隐式类型的转换
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
如果用引用传参,红框能过,蓝框过不了, 因为蓝框发生了隐式类型的转换,传过去的是具有常属性的拷贝值,而引用前面没加const说明引用对象权限大,常属性权限小,所以会报错
要解决这个需要加const
第二种情况
2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
此时没用关键字explicit修饰
使用之后会报错,因为禁止了隐式类型转换
特点:生命周期只有这一行。
只调用构造函数和析构函数,所在行结束后立即调用析构函数
匿名对象可以这样使用,直接调用函数
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
实现一个类,计算程序中创建出了多少个类对象。
若用全局变量统计,则存在一个问题,就是全局变量谁都可以使用用静态处理就能实现只能自己的类来使用
class A { public: A() { ++_scount; } A(const A& t) { ++_scount; } ~A() { --_scount; } static int GetACount() { return _scount; } private: static int _scount;//声明 };
只声明,没定义会报错
用初始化列表初始化(定义)静态变量也会报错
给缺省值也会报错,因为缺省值是给初始化列表的,该静态变量的定义没用在初始化列表,所以会报错
静态变量只能在类外面定义初始化
此时程序正常运行
特性
1. 静态成员为所有类对象所共享(属于这个类的所有对象),不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
对第一句话的解释,a1,a2,a3共享一个scount,scount属于这个类的所有对象,也属于整个类最后一个为5是因为,前面的都打印完了,去执行最后一个程序,要调用一次构造函数,所以是5
静态成员变量,属于整个类,生命周期:整个程序运行期间,存在静态区
静态成员函数:没有this指针,不能去访问非静态成员,只能访问静态成员
静态函数可以创建一个对象再调用,也可以直接不创建调用
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
class A{ public: A() { _sum+=_i; _i++; } static int GetCount() { return _sum; } private: static int _i; static int _sum; }; int A::_i=1; int A::_sum=0; class Solution { public: int Sum_Solution(int n) { A a[n]; return A::GetCount(); } };
变长数组a[n]是为了让构造n次,
设计一个只能在栈上定义对象的类
我们创建的so1在栈上,so2在静态区
如果这样显示定义构造函数对于so1,变量定义在了栈上,对于so2,变量定义在了静态区,它们会在自己的区域去执行该构造函数
因此我们不公开构造函数,私有成员都在栈上,另创建一个函数来获取构造后的变量, 这样创建的变量就会在栈上
但是如果这样写会出现问题,要创建一个对象才能调用CreatObj,但是创建对象一执行程序就要构造,而构造函数又在CreatObj里面,就会产生鸡和蛋的问题
此时放在静态区即可
class StackOnly { public: static StackOnly CreatObj() { StackOnly so; return so; } private: StackOnly(int x = 0,int y=0) :_x(x), _y(y) {} private: int _x = 0; int _y = 0; }; int main() { StackOnly so=StackOnly::CreatObj(); return 0; }
此时可正常运行
不能直接创建变量,而是要通过访问函数
【问题】
1. 静态成员函数可以调用非静态成员函数吗?不能,因为没有this
2. 非静态成员函数可以调用类的静态成员函数吗?
可以,静态属于整个类的所有对象
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,如流插入
friend ostream& operator<<(ostream& _cout, const Date& d); friend istream& operator>>(istream& _cin, Date& d);
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数友元能少用就少用,尽量用封装
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
友元关系不能继承,在继承位置再给大家详细介绍。
如这个俩个类,类友元之后,Time就能访问Date的私有和公共,Date不能访问Timeclass Time { friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量 public: Time(int hour = 0, int minute = 0, int second = 0) : _hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { // 直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外
部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中
的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。class A { private: static int k; int h; public: class B // B天生就是A的友元 { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { A::B b; b.foo(A()); return 0; }
A的大小还是它自身大小
A里面没有B的成员,A的大小跟B没有任何关系,用B去定义成员之后A里面才会有B
B定义在A的里面:
1.受A的类域限制,访问限定符(A和B同等地位,只不过是受到了A的限制)
3.A的静态成员在B里面可以直接访问,静态成员突破类域就能访问,这里不需要指定类域(不需要A::B),因为B是A的友元,当然也可写成A::B k.a
上面习题修改
class Solution { class A{ public: A() { _sum+=_i; _i++; } }; public: int Sum_Solution(int n) { A a[n]; return _sum; } private: static int _i; static int _sum; }; int Solution::_i=1; int Solution::_sum=0;
这种传参方式不会调用拷贝构造
class W { public: W(int x = 0) { cout << "W()" << endl; } W(const W& w) { cout << "W(const W& w)" << endl; } W& operator=(const W& w) { cout << "W& operator=(const W& w)" << endl; return *this; } ~W() { cout << "~W()" << endl; } }; void f1(W w) { } void f2(const W& w) { } int main() { f1(W()); return 0; }
本来是构造+拷贝构造——编译器进行了优化,优化成了直接构造
结论:一个表达式步骤中,连续的构造一般都会优化,合二为一
构造w2+再类型转换(拷贝构造)优化为直接构造
但类类型做返回值类型时,不会优化
这种情况又不一样
本来应该时调用f3函数,给ret构造,然后return时,拷贝构造,赋值给w1时,再发生一次拷贝构造,也就是一次构造,俩次拷贝构造,但实际只发生了一次拷贝构造,是因为这里产生看优化一个表达式步骤中,连续的构造一般都会优化,合二为一,
编译器省略掉了返回时的拷贝构造,直接把ret返回,但这种优化会出问题,
出了作用域对象以后,ret就销毁了,如果这里的内存被其它地方拿去使用,就成野指针了。
返回时临时拷贝的这个对象,如果比较小4-8字节就存在寄存器中,如果比较大就在上一层栈帧,这里的优化是让w1在f3结束前充当tmp价值(f3函数栈帧还没有结束,就把值已经返回了,不需要中间的拷贝)
这俩种方式都一样
单独调用f3函数,没有把省略返回时候的拷贝值
class Widget { public: Widget() { cout << " Widget()" << endl; } Widget(const Widget& d) { cout << "Widget(const Widget& d)" << endl; } ~Widget() { cout << "~Widget()" << endl; } }; Widget f(Widget u) { Widget v(u); Widget w = v; return w; } int main() { Widget x; cout << endl; Widget y = f(x); return 0; }
上面总共进行了1次构造,4次拷贝构造
本来是五次拷贝构造,发生了优化,把返回时候的拷贝构造给优化了
一次构造,7次拷贝构造
本来应该是九次拷贝
这里优化了俩次return 时候创建临时对象的拷贝构造
如果调用Realse版本,结果又不一样
Realse版本下是5次
打印观察,Realse版本优化了W w=v这一步
如果不在一个步骤里面,就不会发生优化