C++:类和对象(含各编译器对编译过程的优化解释)详解[后篇]
目录
一.初始化列表
1.初始化列表(显示初始化)优势:
2.缺省值:
3.对象初始化的顺序:
二.static静态成员
三.友元,内部类和匿名对象
1.友元:
2.内部类:
3.匿名对象:
四.(隐式)类型转换与编译器的优化
1.类型转换:
2.隐式类型转换及编译器优化:
一.初始化列表
在具体介绍编译器是如何优化构造,析构等函数前先把上一篇类和对象没介绍的一些相关的知识再补充说明一下:首先就是这个初始化列表,初始化列表简而言之就是构造函数的另外一种形式,其主要作用也就是对类的对象的实例化:
#include<iostream> using namespace std; class DATE { public: //这里是为了更好的比较两种构造函数初始化的区别才将两个DATE函数的参数设置成一样,实际运用中不能这样写重载函数,会造成编译紊乱 //一般初始化(隐式初始化)在函数体内实现 DATE(int year = 1, int mouth = 11, int day = 111) { _year = year; _mouth = mouth; _day = day; } //初始化列表初始化(显式初始化) DATE(int year = 1, int mouth = 11, int day =111) :_year(year) , _mouth(mouth)//这里表示直接将 year的值赋予给_year { _day = day; } private: int _year; int _mouth; int _day; };
1.初始化列表(显示初始化)优势:
简而言之就是具有更高的效率,因为拥有直接赋值的特点,所以相比一般构造函数可以有效避免在函数体内赋值的操作(但有一点需要注意:有三种变量必须用初始化列表初始化: 引用成员变量,const成员变量,没有默认构造的类类型变量)
2.缺省值:
C++11支持在成员变量声明时给定缺省值,这个缺省值是给没有显示在初始化列表里的成员变量使用的
#include<iostream> using namespace std; class DATE { public: DATE(int x) :a(x) { cout << a << " " << b << endl; } private: int a = 1; //b没有在初始化列表里面初始化,因此这里的2就是表示b的缺省值 int b = 2; }; int main() { DATE m(0); return 0; }
3.对象初始化的顺序:
初始化列表中,按成员变量在类中的声明顺序进行初始化,与成员在初始化列表中出现的先后顺序无关,因此建议变量的声明顺序和在初始化列表中的顺序保持一致
class DATE { public: DATE(int x) :b(x) ,a(x) { cout << a << " " << b << endl; } private: int a = 1; int b = 2; }; //如上述代码所示,先初始化a再初始化b,只与声明顺序有关
#include<iostream> using namespace std; class A { public: A(int a) :_a1(a) , _a2(_a1) { } void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2 = 2; int _a1 = 2; }; int main() { A aa(1); aa.Print(); } //先初始化a2,再初始化a1。那这样的话a2在初始化的时候他的值和a1的值相等,而a1此时的值是随机值,在这之后a1进行初始化,他的值等于1. //因此输出结果为1和随机值。
二.static静态成员
成员变量可以用static修饰,称为静态成员变量,这种变量为这种类的所有对象所共享,不属于某个具体的对象,存放在静态区,因此静态成员不会占用该类的内存,自然其也不会在类中进行初始化(在类外初始化),所以也用不着声明时候的缺省值,也更不会谈上走初始化列表
成员函数也可以用static修饰,称为静态成员函数,静态成员函数没有this指针。这种函数中可以访问其他静态成员,但是由于没有this指针,不能访问非静态成员。而非静态的成员函数,可以任意访问静态成员和非静态成员
在类域外可以访问静态成员,用类名::静态成员或对象.静态成员的方式(这样的直接访问仅限于静态成员是在public共有属性里,如果在private私人属性里就只能通过成员函数的间接访问(代码如下)才行了,由此可见静态成员也是类的成员,仍受public、private等的限制)
#include<iostream> using namespace std; class A { public: A(int x) :a(x) { } //调用静态成员函数访问私有成员变量a和静态成员b,由于静态成员函数没有this指针,所以只能通过添加引用类型的参数才可以访问非静态成员 static void print(const A& m) { cout << m.a << " " << b << endl; } //在不添加参数的情况下间接访问静态成员 static int get() { return b; } private: int a = 1; static int b; }; //类外初始化 int A::b = 100; int main() { //这里的涉及的类型转换和构造析构函数在编译器中的优化后续会讲到 A m = 10; m.print(m); cout << m.get(); return 0; }
三.友元,内部类和匿名对象
1.友元:
友元简而言之是一种在类域里面的声明,可以用于链接各个不同的域,友元分为友元函数和友元类,即在函数声明或类声明的前面加上关键字
friend
,这样的
一个友元声明放在了某个类中,则此友元就可以访问该类的所有成员了,不过需要注意的是,域友元是仅仅支持单向访问的,就像我认定你是我的朋友,我可以访问你的所有,但你不一定认为我是你的朋友,因此也就不会想去访问我的所有#include<iostream> using namespace std; class A { //友元声明 friend void func(const A& a); friend class B; //func是A的友元函数,func函数可以访问A类型对象的所有成员了 //B是A的友元,B可以访问A,但A不能访问B private: int x = 5; int y = 24; }; void func(const A& a) { cout << a.x << ' ' << a.y << endl; } class B { public: B(const A& a) { //B类型对象可以访问A的private成员 n = a.x; m = a.y; } private: int n; int m; };
2.内部类:
把一个类声明在另一个类里面,这样的声明下处在别的类内部的类就叫内部类,内部类是外部类的友元,如果我们把内部类声明为外部类的私有成员,那么这个内部类就可以成为外部类的私有类,但还有一点需要再强调一下,内部类依旧具有单向访问性,比如我设两个类分为A,B:B可以访问A的所有成员,而A能否访问B的成员取决于B的成员的访问权限,如果B的成员是public,A可以访问;如果是private,则需要将A声明为B的友元,或者通过public方法间接访问
class A { private: static int _k; int _h = 1; public: class B // B默认就是A的友元 { public: void foo(const A& a) { cout << _k << endl; //OK cout << a._h << endl; //OK } int _b1; }; }; int main() { A::B b; }
3.匿名对象:
写成类型 对象名(实参)实例化出对象,这样定义的对象叫做有名对象,而类型(实参),这样定义出来的对象叫做匿名对象
匿名对象的声明周期只有当前一行,一般如果我们需要定义一个对象仅供当前用一下,就可以定义匿名对象,当这行代码结束后,匿名对象销毁,也就会调用类的析构函数了#include<iostream> using namespace std; class A { public: A(int x = 1) :c(x) { cout << x << endl; } void print(const A& R) { cout << R.b << endl; } private: static int b; int c = 1; }; int A::b = 100; int main() { A bb(2);//输出2 A(3);//输出3 A().print(bb);//因为A()没有传入值,因此在先调用构造再调用print的时候会先输出1 return 0; }
四.(隐式)类型转换与编译器的优化
1.类型转换:
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
构造函数前面加explicit就不再支持隐式类型转换。
类类型的对象之间也可以隐式转换,需要相应的构造函数支持
#include<iostream> using namespace std; //内置类型转换为类类型对象 class A { public: //前面加上explicit,这个构造函数就不支持类型转换 A(int a, char b) { _a = a; _b = b; } A(int a) { _a = a; _b = 'x'; } A(char b) { _a = 6; _b = b; } int Geta() { return _a; } char Getb() { return _b; } private: int _a; char _b; }; ostream& operator<<(ostream& out, A& a) { out << a.Geta() << ' ' << a.Getb(); return out; } int main() { A a1 = { 1,'a' }; cout << a1 << endl; A a2 = 2; cout << a2 << endl; A a3 = 'x'; cout << a3 << endl; return 0; }
2.隐式类型转换及编译器优化:
#include<iostream>
using namespace std;
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;
//this为隐藏的左值指针
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
//手动析构函数
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
int main()
{
// 构造临时对象,临时对象再拷贝构造aa1,优化为直接构造
A aa1 = 1;
// 传值传参
// 无优化
A aa2;
f1(aa2);
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
/// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
//cout << endl;
// 传值返回
// 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022 debug)
f2();
cout << endl;
// 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022 debug)
A aa3 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
上面这两张图片一个是对运行结果的演示(即优化之后的代码结果),另外一张则是展示了其中几例连续构造的具体过程,其中有两点我需要在补充说明一下:
一是在类型转换中的输出流的运算符重载:
class() { } ostream& operator<<(ostream& out, A& a) { out << a.Geta() << ' ' << a.Getb(); return out; } int main() { } //流插入<<和流提取>>其实有两个操作数,<<是cout和一个变量,>>是cin和一个变量 /cout本质上是ostream类的一个对象,cin本质上是一个istream类的对象 //重载<<和>>时,必须重载为全局函数,如果重载为成员函数,this指针默认占据了第一个形参的位置,第一个形参是操作符的左侧运算对象,调用时就变成了对象 << cout或对象 >> cin,不符合使用习惯和可读性。所以重载为全局函数,把ostream或istream放到第一个形参的位置就好了,第二个形参放当前类类型对象。
二则是类中A(int a = 0)和A(const A& aa)的区别,以及其在什么情况下调用
A(int a = 0):
是默认构造函数,允许在创建对象时省略参数,使用默认值0
调用情况:显式调用时传递0或另一个整数,或者隐式调用时没有传递参数
例如:A obj; A obj(10);
A(const A& aa):
是拷贝构造函数,用于通过另一个同类型对象初始化新对象
调用情况:对象作为函数参数按值传递、返回值、初始化列表引用其他对象等
例如:A obj2(obj1); void func(A a) { ... } func(obj1);
ok,类和对象的基础知识就到这里,话不多说
全文终