每日鸡汤:
🚀 世界会向那些有目标和远见的人让路。
目录
🏆一、初始化列表
👓1.1再谈构造函数
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
对于这种构造,很显然是不会出问题的,但是我如果有个这样的类型呢?
很明显,当我们的成员变量有const修饰的时候,我们的构造函数居然会报错,说明它无法处理这种情况,那怎么解决这种初始化问题呢?
👓1.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
class DateB
{
public:
DateB(int year = 1, int month = 1, int day = 1,int second=20)
:_year(year)
,_month(month)
,_day(day)
,_second(second)
{
}
private:
int _year;
int _month;
int _day;
const int _second;
};
这样,我们就对const修饰的也完成了初始化。
当然初始化列表不止这些用处,这里就先简单一下初始化列表的性质。
1、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
2、构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
🖊①初始化列表和构造函数混搭
很简单,就是同时使用呗。
class DateB
{
public:
DateB(int year = 1, int month = 1, int day = 1, int second = 20)
:_year(year)
,_second(second)
{
_year = 100;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
const int _second;
};
我用初始化列表初始化const修饰的成员_second,构造函数体赋值其他成员。这里我初始化列表_year,之后在构造函数体内又对它赋值,那么_year最后是初始化列表的值还是构造函数体内赋的值呢?答案肯定是构造函数体赋的值,因为初始化列表先给初值,然后构造函数又赋值,初始化列表先于构造。
🖊②初始化列表和默认构造函数
当初始化列表时,如果没有传参对于内置类型,有缺省就用缺省,没有就用它的随机值;对于自定义类型,调用它的默认构造函数,没有默认构造函数就会报错。
初始化列表和缺省值的搭配也是常见的写法,如果没有缺省,且没有传参,那么在初始化列表时,就会初始化为随机值。
这里_year和_month我既没有给缺省,也没有初始化,所以就是随机值,对于_day给了缺省,_second进行了初始化。
那么,如果我的成员变量是自定义类型,它会调用它的默认构造函数,如果这个自定义类型没有默认构造函数就会报错。
这里自定义类型A就没有它的默认构造函数,所以会报错,我们再来回顾一下什么叫默认构造函数:没有参数,或者有带缺省的参数的构造函数就是默认构造函数,对于需要传参,不给缺省的就不是。
怎么解决呢?一种就是把自定义类型A改成默认构造函数,就是让它缺省,或者完全不给参数,这是一种方式,我们也可以采取另一种方式:初始化列表。
当然了,初始化列表也可以给默认构造函数(带缺省)传参,那么就会以我们传的参数为准进行初始化。
🖊 ③初始化列表和引用成员变量
引用成员变量是我们要谈的第三个必须要在初始化列表初始化的成员变量,前两个分别是const成员变量和自定义类型成员(且该类没有默认构造函数)。
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
class DateB
{
public:
DateB(int ans,int a)
:_second(10)
, _ans(ans)
,_a(a)
{
}
private:
const int _second;
A _a;
int& _ans;
};
🖊③初始化列表的次序
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
什么意思呢?我们先来看一段代码:
尽管在初始化列表中,我们的次序意思是先初始化_a1,再初始化_a2, 然而结果好像并非如此,按照我们的感觉,结果应该是1,1.出现这样的结果说明编译器并非按照我们所设定的顺序,而是按照在类中声明的次序,先初始化_a2,再初始化_a1,因为_a1刚开始是随机值,所以_a2也是随机值,然后_a1初始化为1.
这个知识点挺绕的,这里我给大家再总结一下:
1、构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2、当类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(该类没有默认构造函数时)
3、当初始化列表时,如果没有传参对于内置类型,有缺省就用缺省,没有就用它的随机值;对于自定义类型,调用它的默认构造函数,没有默认构造函数就会报错。
4、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
5、尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
🏆二、explicit关键字
👓2.1隐式类型转换
🖊①隐式类型转换
我们在创建类对象的时候,有好几种方式,默认构造,传参构造,两种拷贝构造:
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) ,_month(month) ,_day(day) { } private: int _year; int _month; int _day; }; int main() { Date d1;//默认构造 Date d(2022, 10, 13);//传参构造 Date d2(d1);//拷贝构造1 Date d3 = d2;//拷贝构造2 return 0; }
这里我们还有一种隐式类型转换的方式进行构造:
Date d4 = 2022;// 这里就有Date的临时对象构造+ 拷贝构造 const Date& d5 = 2022;
这里的d4的构造过程是:
d5 也是构造临时变量,拷贝构造d5,但是这里是引用临时变量,之前博主提到过临时变量具有常量性:
在C语言阶段,我们提及过隐式类型转换,比如:
int main()
{
int i = 1;
double b = i;
const double& c = i;//引用
return 0;
}
这里,我们知道把整型i的值赋给浮点型b,这里是生成一个临时变量tmp,然后b拷贝赋值。因为临时变量具有常量性,所以我们在引用时就要加const,这是之前提及的。他这里就涉及到了隐式类型转换。
所以这里d5的构造需要加const。
🖊②支持多参数的转换
我们上面只是传了一个参数,还有两个成员变量是使用缺省值初始化,我们可以通过隐式类型转换给他们也进行传参吗?可以的!
我们这里不是这种形式:
Date d4 =2022,10;
Date d5=(2022,10,12);
这里类似于数组,支持这种形式:
Date d6={2022,10}; Date d7={2022,10,12};
差点忘了说:这个标准是C++11才支持的,所以比较老版的编译器比如vc6.0,vs2008是不支持的。
🖊③隐式类型转换的应用场景
可能有的老铁觉得,这有啥用呢?创建一个对象还多构造一个临时对象,得不偿失啊,既然它有这个语法,肯定是有的放矢的。
在C++类中,有string类(字符串),比如我想在字符串中插入一段字符串,如果不使用隐式类型转换,我们需要先创建一个对象,再push:
string s1("hello");
push_back(s1);
string s2("world");
push_back(s2);
而如果我们使用这个特性,是可以直接push,更加简洁的:
push_back("C++");
也是有一定的应用场景的。
👓2.2 explicit 关键字
explicit修饰构造函数,禁止类型转换,也就是说,没有使用explicit修饰的构造函数,具有类型转换作用,使用explicit修饰的,不能隐式类型转换。
🏆 三、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
在C语言时期,static主要用于改变变量的生命周期,将数据存储到静态区,以及限制函数不能链接到其他文件。在C++时期,static又补充了哪些新的语法呢?我通过一个面试题来讲解:
面试题:实现一个类,计算程序中创建出了多少个类对象。
类对象的创建方式有两种:1、构造函数;2、拷贝构造函数。所以我们只需要设定一个全局变量,当调用一次构造函数的时候++,调用一次拷贝构造函数的时候++。全局变量的值就是创建了多少个类对象。但是耦合性比较高,因为全局变量能被随意使用,在工程中安全性不高,所以,我们就想在类里面创建一个可以发挥类似于全局变量功能的变量:static成员。
👓3.1static成员变量
class A
{
public:
A()
{
++_count;
}
A(const A& aa)
{
++_count;
}
~A()
{
--_count;
}
private:
static int _count;
};
我们就实现了在类里面计算创建了多少个类对象。还有一个问题,就是static成员变量在哪定义呢?static成员在类外定义!定义时不需要加static,但要加类域它的初始化格式为:
类型 类::成员 = 初始值;
🖊①static成员定义在静态区
类中static成员变量的生命周期是全局的,但是作用域受类域限制,类里面的静态只能在类里面使用,static成员创建在静态区,不在栈上,并且静态成员变量只会创建一个。
如果我创建两个对象,那么会生成两个静态成员变量吗?
class A
{
public:
A(int a=1)
:_a(a)
{
++_count;
}
A(const A& aa)
{
++_count;
}
~A()
{
--_count;
}
private:
static int _count;
int _a;
};
int A::_count = 0;
int main()
{
A a1;
A a2(2);
return 0;
}
不会的,只会有一个静态成员变量创建,因为对象里面不存储静态成员变量,因为它不在栈上。所以静态成员变量只会创建一个。
🖊②静态成员函数
我想得到创建了几个对象,怎么访问到_count呢?我们可以在类里面写一个static函数,得到_count。
static int Get_count()
{
return _count;
}
看到这里,不知道有没有老铁发现问题,我们没有创建对象,直接通过类域调用了static函数。为什么可以这样呢?因为static函数不包括this指针,所以我们可以通过类直接调用。
静态成员变量和静态成员函数是配套使用的。
我们可以看一道oj题,巧妙地利用了static特性。
牛客:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判 断语句(A?B:C)
这道题目,限制了很多条件不能使用,我们可以通过递归来求解,也可以用刚学过的static成员来求解,我们设两个static成员,一个用于统计创建了多少个对象,一个用于加和求出结果。
class Sum_count
{
public:
Sum_count()
{
_count++;
_ans+=_count;
}
static int Get_ans()
{
return _ans;
}
private:
static int _count;
static int _ans;
};
int Sum_count:: _count=0;
int Sum_count:: _ans=0;
class Solution {
public:
int Sum_Solution(int n)
{
Sum_count m[n];
return Sum_count::Get_ans();
}
};
总结一下:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员,不需要通过对象访问静态成员函数。
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
🏆四、友元类
之前在讲解>>重载和<<重载的时候用过友元声明,这里正式介绍友元。友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
特点:
1、友元关系是单向的,不具有交换性。
比如A类和B类,如果在B类中声明A类为它的友元类,那么在A类中可以直接访问B类的私有成员变量,但想在B类中访问A的私有成员变量是不可以的。
2、友元关系不能传递
如果C是B的友元,B是A的友元,则不能说明C是A的友元。
3、友元关系不能继承。(后面再说)
class 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(外部类)=外部类,和内部类没有任何关系。
对于第三点、我在这里演示一下:
🏆 六、匿名对象
有名字的对象叫有名对象,没有名字的显然就是匿名对象。我们先来看一下匿名对象的使用:
👓6.1匿名对象的特点
匿名对象的生命周期只有它所在的一行,到了下一行就会自动调用析构函数。
有名对象会在进程结束时调用析构,匿名对象在调用后就析构。
👓6.2匿名对象的使用场景
匿名对象的特点看似比较鸡肋,但是存在即合理,他还是有使用场景的,他就像一个一次性用品,如果我们不想创建对象,只是单纯的调用类中某个函数就可以用匿名对象。
class A
{
public:
void solution()
{
cout << "hello" << endl;
}
};
int main()
{
A().solution();
return 0;
}
🏆七、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
👓7.1隐式类型优化
按理来说这里应该是先隐式类型转换,构造一个临时变量,在传值传参时,再调用拷贝构造,编译器优化为直接构造。
这里也是如此,正常流程是匿名对象构造,然后拷贝构造,优化为了直接构造临时对象。
👓7.2传值返回优化
传值返回,因为出栈会销毁栈中变量,所以需要拷贝构造中间临时变量,然后再拷贝构造给aa2,但是这里优化只调用了一次拷贝构造。
👓7.3赋值重载无法优化
编译器优化的前提是不影响正确性。前面的优化是因为中间临时变量创建了也没用,因为创建了它再拷贝构造,然后销毁,不如直接拷贝构造。
👓7.4合理运用优化
我们要尽量运用编译器的优化细节,这些细节会提高性能,像上面这些,虽然只是细节的优化,但是我们合起来,就有了极致性能的优化!!
这些都是代码的细节,这只是一个简单的类,优化可能不太明显,如果是栈,树这些存储数据量比较大的拷贝,这些细节会大大提高代码的效率。上面这些细节的讲解都在《深度探索C++对象模型》中讲解,感兴趣的老铁可以翻阅一下,里面有很多编译器的细节。