目录
1.初始化列表
1.1 构造函数体内的赋值
1.2 初始化列表
1.对象整体定义和成员变量定义的区别
2.初始化列表的写法
1.3 和C++11的联系
1.4 针对初始化列表的建议
2.静态成员
2.1 静态成员变量
1.概念
2.特性
2.2 静态成员函数
1.概念
2.特性
3.友元
3.1 友元函数
3.2 友元类
4.内部类
4.1 概念
4.2 特性
5.隐式类型转换和explicit关键字
5.1 隐式类型转换
1.支持隐式类型转换的前提
2.隐式类型转换的底层原理
2.1 复习:C语言的隐式类型转换和const引用
2.2 自定义类型的隐式类型转换
3.隐式类型转换的场景
第一种:string类的构造场景
第二种:string对象的传参场景
5.2 explicit
6.匿名对象
6.1 匿名对象的创建
6.2 匿名对象的特点
6.3 匿名对象的场景
1.方便类中成员函数的调用
2.某些传参场景(同上隐式类型转换)
7.拷贝对象时的编译器优化
7.1 连续的构造+拷贝->一次构造
7.2 连续的拷贝+拷贝 ->一次拷贝
7.3 连续的拷贝+赋值 ->不能优化
1.初始化列表
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;
};
1.2 初始化列表
1.对象整体定义和成员变量定义的区别
当我们写下Date d时,d一个对象被整体定义了,但是d里面的成员定义好了吗?并没有!只有在构造函数调用时的初始化列表中,成员变量才被定义。(比喻:我们把房子建好了,这个房子相当于d对象整体,但是房子里面的各个房间并没有装修好,这些房间也就是各个成员)
2.初始化列表的写法
成员变量的定义和初始化都在初始化列表中完成,那么我们怎么写呢?
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
初始化列表和函数体内的初始化是可以混着用的。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意几点:
-
我们自己不显示写初始化列表时,编译器自己有个初始化列表,它的初始化列表针对成员变量只有定义没有初始化。
-
针对某些特殊成员,他们必须在定义的时候初始化,比如:引用类型、const类型;如果类中有这些成员而我们没有显示写初始化列表时,这些成员就只有初始化而没有定义,编译就会出错,所以这两种成员我们只能在初始化列表中初始化。
-
如果我们不显示写初始化列表:其他的内置类型(除引用、const类型)也会定义,但是只有定义没有初始化,所以这些内置类型会给随机值;而自定义类型,调用它的默认构造。
-
如果自定义类型没有可用的默认构造,并且我们没有显示写初始化列表,也会导致编译出错,所以针对这种自定义类型,我们只能在初始化列表中显示调用他的其他构造函数。
-
总结:必须用初始化列表来初始化的三种类型:
-
引用类型
-
const类型
-
没有默认构造的自定义类型
-
1.3 和C++11的联系
C++11针对内置类型,在声明处是可以给缺省值的,但是这个为什么叫缺省值?和缺省参数有关系吗?
-
这里的缺省值就是给初始化列表用来初始化内置类型的!
至于为什么叫缺省值,因为他和缺省参数一样是个备胎,我们显示写初始化列表给了值以后,就不用缺省值了。
-
初始化列表的定义初始化顺序是按照类中的声明顺序来的。
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前面,所以我们先初始化a2,再初始化a1,所以a1为1,a2为随机值。
1.4 针对初始化列表的建议
其实在绝大多数情况下,我们都建议写初始化列表。因为不管你是否使用初始化列表,对于自定义类型的成员变量,一定会先使用初始化列表初始化。
-
前面说明,这三种类型必须使用初始化列表初始化:引用类型、const类型、没有默认构造的自定义类型。
-
但是有默认构造的自定义类型也建议使用初始化列表,因为我们在函数体中不太好显示初始化自定义类型。比如下面代码:
class A
{}
class B
{
public:
B(int a)
//:_aobj(a) 初始化列表这么写
{
A tmp(a);
_aobj = tmp;
}
private:
A _aobj;
}
这种写法显然有点孬,不如初始化列表高效。
-
还有内置类型也是建议使用初始化列表的,但并不是全部,比如涉及资源申请的指针类型,有时候比较冗余的话还是在函数体中赋值比较正常。
2.静态成员
2.1 静态成员变量
1.概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
2.特性
-
静态成员变量不属于任何一个对象,而是属于整个类的,或者说为这个类的所有对象所共享。
-
静态成员变量的使用:
类名::静态成员变量
对象名.静态成员变量
-
静态成员变量的定义:
-
静态成员变量能在声明位置给值吗?结合前面所学:不可以!
声明位置给的缺省值是给初始化列表的,而初始化列表是在对象定义完后用来定义初始化成员变量的,但是静态成员变量属于一个对象吗?显然不是,所以静态成员变量不能在类里面定义。
-
静态成员变量只能在类外定义,声明和定义是要分离的,具体怎么写,如下:
class A { public: //.... private: int _a = 1; static int _b;//静态成员变量声明 }; int A::_b = 0;//静态成员变量定义
注意几点:
-
不要忘记指定类域。
-
定义时要加类型,但不要加static。
-
这里在声明定义时访问静态成员变量并不受访问限定符的限制,只是定义而已。
-
-
-
静态成员变量也受访问限定符的影响。
2.2 静态成员函数
1.概念
用static修饰的成员函数,称之为静态成员函数。
2.特性
-
静态成员函数不属于任何一个对象,而是属于整个类的,或者说为这个类的所有对象所共享。
-
静态成员函数的使用:
类名::静态成员函数
对象名.静态成员函数
class A { public: //.... static int getStatic() { return _b; } private: int _a = 1; static int _b; }; int A::_b = 0; int main() { A a; int a = A::getStatic(); int b = a.getStatic(); } ```
-
静态成员函数不属于任何一个对象,也不需要一个对象来调用,那么它的函数参数有this指针吗?
显然没有!因此静态成员函数不能访问非静态成员变量、调用非静态成员函数。
-
静态成员函数也受访问限定符的影响。
-
总结:这么理解静态成员变量和静态成员函数更加清晰:
静态成员变量和静态成员函数其实就是受限制的全局变量和全局函数,他们并不在类中,包括静态成员变量(所以只有非静态成员变量是在类中的)。他们受什么限制呢?类域限制和访问限定符限制。
3.友元
针对某些场景我们在类外需要访问类中的私有成员,我们目前有两种方法解决:
-
变私有为公有。
-
添加这些私有成员的get函数。
方法一过于暴力对封装的破坏性大,方法二又不适合C++(Java经常这么用),这时候就用到了C++的一种突破封装的语法:友元。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装(但相比直接修改访问限定符,破坏程度较小),所以友元不宜多用。 友元分为:友元函数和友元类
3.1 友元函数
友元函数的经典场景就是operator<<和operator>>的重载。我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date
{
public:
//...
//友元函数
friend ostream& operator<<(ostream& out, const Date& d);//cout << d1
friend istream& operator>>(istream& in, Date& d);//cin >> d1
private:
//...
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << ":" << d._month << ":" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
-
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
-
友元函数可访问类的私有和保护成员,但不是类的成员函数。
-
友元函数不能用const修饰。
-
友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
-
一个函数可以是多个类的友元函数。
-
友元函数的调用与普通函数的调用原理相同。
3.2 友元类
class Time
{
friend class Date; //声明Date类为Time类的友元类,则在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)
{
// 直接访问Time类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
-
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
-
友元关系是单向的,不具有交换性。(比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。)
-
友元关系不能传递。(如果C是B的友元, B是A的友元,不能说明C时A的友元。)
-
友元关系不能继承。
4.内部类
4.1 概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
4.2 特性
-
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
-
外部类对内部类没有任何优越的访问权限。
-
内部类天生为外部类的友元类,但是外部类不是内部类的友元。
-
内部类的实例化要指定外部类的类域。
-
内部类可以定义在外部类的public、protected、private限定符中。
-
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象名/类名。
-
sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
5.隐式类型转换和explicit关键字
5.1 隐式类型转换
1.支持隐式类型转换的前提
构造函数不仅可以初始化对象,而且对于单参数构造函数(单个参数或者因为缺省参数也可以传单个参数),还具有类型转换的作用。比如Date类:
class Date
{
public:
Date(int year = 1970,int month = 1, int day = 1)
:_year(year)
,_month(_month)
,_day(_day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d = 2023;//针对单参数构造函数,支持隐式类型转换
return 0;
}
2.隐式类型转换的底层原理
2.1 复习:C语言的隐式类型转换和const引用
-
C语言的隐式类型转换: double b = 3.14; int a = b;
-
类型转换(强制类型转换和隐式类型转换)在底层上会产生一个临时变量,这个临时变量具有常量的属性。类型转换是并不会真的改变原来变量的类型的。
-
所以使用引用时,是要加上const的:double b = 3.14;const int& a = b;
2.2 自定义类型的隐式类型转换
因为构造函数的特殊性而让自定义类型也能隐式类型转换,底层也是这个原理:
-
Date d = 2023; 底层上会先使用2023构造一个临时对象Date tmp(2023, 1, 1);然后临时对象tmp拷贝构造给d。(所以这个过程是构造+拷贝,但是编译器会优化成一次构造,具体后面会系统说)
-
这个临时对象tmp具有常量的属性,所有涉及到引用时,要加const:
const Date& d = 2023;
3.隐式类型转换的场景
第一种:string类的构造场景
C++有个string类,它的构造函数可以认为是这样的:string(const char* str = "");
一般我们是这么构造string类的:string str("hello world");
但是我们也可以这么构造:string str = "hello world";
这里就是涉及了隐式类型转换。
第二种:string对象的传参场景
还有一种场景,就是string类的传参,拿插入一个string对象为例:
一般来说Push只能这么使用:
vector<string> v;
string str("string");
v.Push(str);
但是函数接口这么写的话就不一样了:void Push(const string& str); 也就是要加上const,以前说过const有两个作用:
-
如果函数体中不用修改str对象,起到防止误操作修改str对象的作用。
-
string类的const对象也可以传进来。
现在又多了一种:可以让 涉及隐式类型转换的临时对象(或者匿名对象)也传进来,也就是这种场景:
vector<string> v;
v.Push("string");
这里传参过程是这么传的:cosnt string& str = "string";中间会产生一个临时对象,临时对象具有常性,所有只能用const引用接收,如果函数接口只是正常的string引用是传不进来的。
5.2 explicit
用explicit修饰构造函数,将会禁止构造函数的隐式类型转换。
6.匿名对象
6.1 匿名对象的创建
//针对A类
//普通对象创建:
A a;
//匿名对象创建:
A();
A aa();//编译器识别成函数声明
注意:无参的匿名对象构造,也是要加()的。
6.2 匿名对象的特点
-
没有名字。
-
生命周期只有一行,下一行他就会自动调用析构函数。
-
使用匿名对象中间会先构造一个临时对象。这里是和隐式类型转换相似的,所以匿名对象也可以这么用:
string str = string("hello world");
const string& str = string("hello world");
6.3 匿名对象的场景
1.方便类中成员函数的调用
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
Solution().Sum_Solution(10);
return 0;
}
这样就可以不创建对象来调用成员函数了。
2.某些传参场景(同上隐式类型转换)
上面我们说过,对于Push函数接口,如果Push的是string类对象,接口这么写:void Push(const string& str);
这样可以接受隐式类型转换的临时对象,但是他也是可以接受匿名对象的,因为我们可以认为匿名对象也有个临时对象。
class StringContainer
{
public:
void Push(const string& str)
{
//....
}
};
int main()
{
StringContainer sc;
sc.Push("hello");
sc.Push(string("world"));
return 0;
}
匿名对象的生命周期只有一行,但是我们可以认为使用const引用过后可以延长他的生命周期。
7.拷贝对象时的编译器优化
7.1 连续的构造+拷贝->一次构造
场景1
string str = "hello world";
场景2
void fun(string str)
{
//...
}
f("hello");//隐式类型转换:string str = "hello";
场景3
void fun(string str)
{
//...
}
f(string("hello"));//匿名对象场景:string str = string("hello");
7.2 连续的拷贝+拷贝 ->一次拷贝
场景:传值返回的函数
string fun()
{
string str("hello");
return str;
}
string str1 = fun();
7.3 连续的拷贝+赋值 ->不能优化
string fun()
{
string str("hello");
return str;
}
string str1("hello");
str1 = fun();//这里的=是赋值运算符重载 ,不能优化
tmp临时对象的拷贝是拷贝构造,临时对象再赋值给str1对象是赋值运算符重载,两者不能优化成一次拷贝
注意:上述的两种优化都是在同一个表达式中才能优化的,跨表达式不能这样优化。