C++入门☞关于类的一些特殊知识点
涉及的关于类中的默认成员函数的知识点可以看我的这篇博客哦~
C++入门必须知道的知识☞类的默认成员函数,一文讲透+运用
目录
初始化列表
类型转换
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 _yeaer;int _month;int _day;
};
两种初始化的区别:
使用函数体内赋值的方式时,成员变量会先进行默认初始化,然后再在构造函数体内被赋值。也就是在函数体内被赋值时不能称为"初始化",只能称为"赋值",初始化只能进行一次,而构造函数体内可以多次赋值;
如果用初始化列表,则直接跳过默认初始化步骤,一步到位完成初始化
默认初始化:指当对象被创建时,如果没有显式指定初始值,编译器会自动进行的初始化行为。具体行为取决于成员变量的类型。具体行为如下:
-
内置类型(如
int
,double
, 指针等):-
如果变量在全局作用域(或静态存储区),默认初始化为
0
/nullptr
。 -
如果变量在局部作用域(如函数内、类构造函数体内),不初始化,值是未定义的(垃圾值)。
-
-
类类型(如
std::string
, 自定义类):-
调用该类的默认构造函数(如果没有默认构造函数,会编译报错)。
-
两种方式在面对自定义类型的初始化时,还会有效率的差异,初始化列表的效率更高:
使用函数体内赋值的方式,会先去调用自定义类型的构造函数,然后再调用赋值重载将构造的值赋值给成员变量
使用初始化列表的方式,对于自定义类型只会调用一次拷贝构造函数一次性完成初始化
初始化列表的注意事项:
1、每个成员变量在初始化列表中只能出现一次(即初始化只能初始化一次)
2、类中包含以下成员时,它们的初始化必须放在初始化列表位置进行(否则编译报错):
- 引用成员变量(因为引用在定义初始化时必须赋值,且不能更改引用的对象)
- const修饰的成员变量(const的原因与引用类似)
- 自定义类型的成员变量,且该成员变量的类没有默认构造函数时(因为使用函数体内的方式时,对于自定义类型,会先去调用其构造函数)
3、成员变量在类中的声明顺序就是在初始化列表的初始化顺序,与其在初始化列表中的位置顺序无关(★),所以建议初始化列表的顺序和声明的顺序一致
关于第三条:如下述代码,会得到错误的结果,因为虽然初始化列表中的顺序是先初始化_a,再用_a的值初始化_b,看着是没错,但是因为成员变量声明时,是先声明的_b,再是_a,所以初始化时会先初始化_b,而不是_a,也就是初始化顺序只和声明的顺序相关,和初始化列表中的顺序无关,但是因为_a还未初始化,所以_b的值就会是随机数
class A
{
public:// 初始化列表A(int a):_a(a),_b(_a+1){}
private:int _b;int _a;
};
另外,C++11还支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的,如:
class A
{
public:A(int a):_a1(a){}
private:int _a2 = 2;int _a1;
};
此时,_a2初始化后的值就是2,相当于_a2也走了初始化列表,只不过用的声明时给的缺省值进行初始化,_a1初始化后的值就是传给参数a的值
总结:
尽量使用初始化列表初始化,因为无论是否显示写初始化列表,每个构造函数都有初始化列表;⽆论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化,如果这个成员在声明位置给了缺省值,初始化列表就会用这个值进行初始化。如果你没有给缺省值,那么编译器对其的初始化是不确定的,且对于自定义类型,使用初始化列表的方式会更加高效一点
成员变量初始化思维导图:
类型转换
C++支持内置类型隐式类型转换为类类型的对象,需要有相关内置类型为参数的构造函数
如下:使用一个int类型的值构造了一个A类型的对象,其中发生了隐式类型转换,将int类型转为自定义类型A
class A
{
public:// 以int类型为参数的相关构造函数A(int a):_a(a){}
private:int _a;
};
int main()
{// 使用int类型的1构造了一个A类型的对象A b(1);return 0;
}
explicit关键字
但是上述的隐式类型转换,如果你不想让它发生可以使用explicit关键字对构造函数进行修饰,那么这种隐式类型就会被禁止
// 此时再想用一个数字去构造A类型的对象就会编译报错
class A
{
public:// 以int类型为参数的相关构造函数explicit A(int a):_a(a){}
private:int _a;
};
对于内置类型隐式类型转换构造自定义类型时,相关内置类型的构造函数必须要有,且需要保证传入该内置类型参数时,可以构造出一个对象
除了内置类型和类类型的隐式类型转换,类类型的对象之间也可以隐式转换,需要相应的构造函数的支持
如下方的将A类型的对象隐式类型转换为B类型的对象进行构造,构造出一个临时对象用来拷贝构造B类型的对象b,但是编译器会优化,所以就变成了直接用A类型的对象构造出了一个B类型的对象,但这个过程是因为有相关类型参数的构造函数支持才完成的
class A
{
public:A(int a1, int a2):_a1(a1), _a2(a2){}int Get() const{return _a1 + _a2;}
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:// 临时对象具有常性,所以需要用const接收B(const A& a):_b(a.Get()){}
private:int _b = 0;
};
int main()
{// { 2,2 }构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造a// 编译器遇到连续构造+拷⻉构造->优化为直接构造A a = { 2,2 }; // C++11之后才⽀持的多参数转化// a 隐式类型转换为b对象// 原理跟上⾯类似B b = a;return 0;
}
static成员
定义:
被static修饰的成员称为static成员,也叫类的静态成员
- 静态成员也是类的成员,受public、protected、private访问限定符的限制
- 突破类域可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数
由static修饰的成员变量称为静态成员变量,由static修饰的成员函数称为静态成员函数。
静态成员变量:
静态成员变量一定要在类外进行初始化,即不在构造函数初始化
静态成员变量为类的所有对象共享,不属于某个具体的对象,不存在对象中,存放在静态区
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表初始化的,静态成员变量不属于某个对象,不走构造函数的初始化列表
静态成员函数:
静态成员函数没有this指针
静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
非静态成员函数可以访问任意的静态成员变量和静态成员函数
场景:统计一个类实例化了多少个对象——使用静态成员变量
#include<iostream>
using namespace std;
class A
{
public:A(){count++;}static int getCount(){return count;}
private:static int count;
};
int A::count = 0;
int main()
{A a1;A a2;A a3;cout << "创建了"<< A::getCount() << "个A类型对象" << endl;return 0;
}
在A类型的构造函数中使用静态成员变量count++进行计数,则每初始化一个对象就会对其进行一次++,最终就会得到实例化出的对象个数,同时注意count变量是在类外进行初始化的,只是在类内声明,且在访问静态成员时都使用了类域::静态成员变量的方式(A::count、A::getCount)
上方代码,如果是非静态的成员函数getCount()的话,就通过对象.静态成员函数的方式获得静态成员变量count的值
// 非静态成员函数
int getCount()
{return count;// 返回静态成员变量
}
// 其余代码一致....
int main()
{A a1;A a2;A a3;cout << "创建了"<< a3.getCount() << "个A类型对象" << endl;return 0;
}
友元
定义:
友元分为友元函数和友元类,在一个类里,给函数声明或者类的声明前面加上friend的修饰,则称其为类的友元函数或者友元类,成为类的友元后可以访问类中的私有成员和保护成员
友元类:
class A
{// 友元声明,B这个类变成A的友元类friend class B;
private:int _a1 = 1;
};class B
{
public:void func(const A& aa){// 友元类B就可以访问A类的私有成员cout << aa._a1 << endl;cout << _b1 << endl;}
private:int _b1 = 3;
};
int main()
{A aa;B bb;bb.func(aa);return 0;
}
成为一个类的友元类后,友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性。(比如上面的B类可以访问A类的私有成员,但是A类就不可以反过来访问B类的非公有成员,因为A类不是B类的友元类)
- 友元关系不能传递(如果C是B的友元, B是A的友元,则不能说明C时A的友元)
- 友元关系不能继承(涉及继承知识)
友元函数:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,即不属于类的成员函数,但需要在类的内部声明,声明时需要加friend关键字,如下
class Date
{// 友元函数声明friend ostream& operator<<(ostream& _cout, const Date& d);friend istream& operator>>(istream& _cin, Date& d);
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
// 自定义Date类的输入输出
ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{_cin >> d._year;_cin >> d._month;_cin >> d._day;return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
1、友元函数可以在类定义的任意地方声明(注意:是在类定义的地方声明,类的定义有声明和定义都在一个文件中实现的方式,也有声明定义分离的方式,如果是分离的方式,要在定义的地方声明友元函数),且它不受任何访问限定符的限制
2、一个函数可以是多个类的友元函数
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
内部类
定义:
如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:
- 内部类默认就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
- 内部类可以访问的外部的所有成员中包括外部类的静态成员,不需要外部类的对象/类名来突破类域访问
- sizeof(外部类)=外部类,和内部类没有任何关系。
内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要是给B类使用时,可以考虑将其设为B类的内部类, 如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
匿名对象
定义:
用类型(实参) 定义出来的对象叫做匿名对象,前面我们使用 类型 对象名(实参) 格式定义出来的叫有名对象
// 假设有一个名为A的类,且可以用int类型隐式转换初始化
int main()
{A aa1(1);// 有名对象A(1);// 匿名对象
}
如果A类的构造是可以无参的,那么A类的匿名对象的创建也不可以省略掉括号,否则语法出错
A();// 无参的A类型匿名对象
注意:匿名对象的生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以使用匿名对象
匿名对象调用场景:
①成员函数调用:
class Solution {
public:int Sum_Solution(int n) {//...return n;}
};
int main()
{// 没有匿名对象时候,需要写两行调用Solution s1;cout << s1.Sum_Solution(10) << endl;// 有了匿名对象直接写成一行调用cout << Solution().Sum_Solution(10) << endl;
}
②函数参数自定义类型,需要给参数缺省值:给匿名对象
void func(A aa = A(1))
{}
延长匿名对象生命周期:
引用匿名对象,延长到和引用的生命周期一致,
但是匿名对象和临时对象一样具有常性,要加const
const A& r = A();
匿名对象就像一个一次性的事物,用完就可丢~
对象拷贝时的一些编译器的优化
现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝
假设一个类的定义如下:
class A
{
public:// 构造函数A(int a = 0):_a1(a){cout << "A(int a)" << endl;}// 拷贝构造A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}// 赋值重载A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}// 析构函数~A(){cout << "~A()" << endl;}private:int _a1 = 1;
};
传参时的编译器优化:
连续构造+拷贝构造优化——>直接构造,省略了一次拷贝
void f1(A aa)
{}
int main()
{// 优化// 按理这里会先构造临时对象,再把临时对象拷贝构造给aa0// 但是这里优化成直接用1进行构造了A aa0 = 1;// 隐式类型转换cout << endl;// 优化// 按理是用1构造一个匿名对象,再把匿名对象拷贝构造给aa// 隐式类型转换,连续构造+拷贝构造->优化为直接构造f1(1);// 优化// 一个表达式中,连续构造+拷贝构造->优化为一个构造f1(A(2));cout << endl;return 0;
}
传值返回时的编译器优化:
两次连续的拷贝构造优化成一次拷贝构造
A f2()
{A aa;return aa;// 不会返回aa,会用aa创建一个临时对象,返回临时对象
}
// 传值返回的优化
int main()
{// 按理需要先用返回值aa拷贝构造一个临时对象,再用临时对象拷贝构造aa2// 优化:两个拷贝构造合二为一为一个拷贝构造// 更新的编译器优化:直接优化成一次构造,直接用aa的值构造aa2A aa2 = f2();cout << endl;// 按理是返回值aa拷贝构造一个临时对象,再用临时对象赋值拷贝给aa1// 优化:直接用aa的值赋值拷贝给aa1A aa1 = 1;aa1 = f2();cout << endl;return 0;
}
完