【C++】类和对象(下)--详解之再探构造函数,友元,static成员,类型转换等

前几节内容我们提到了友元,static等新的知识,本节内容就是对他们进行开展学习,同时也是类和对象的收尾工作,那开始本节内容的学习

一.再探构造函数

之前我们实现构造函数时**,**初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表

初始化列表:

简单来说:构造函数后面就是初始化列表,在进入函数大括号之前执行,用来初始化成员

cpp 复制代码
Date(int y, int m, int d)
	:_year(y),
	_month(m),
	_day(d)
{

}

始化列表的使⽤⽅式是以⼀个冒号开始 ,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式

初始化列表的特点

  • 特点一:一个成员在初始化列表只能写 1 次
cpp 复制代码
Test():a(1),a(5) //会报错,a初始化两次
{}

初始化列表可以理解为变量诞生时赋值,但变量不能诞生两次,所以只能出现一次


  • 特点二:三种成员被强制只能在初始化列表初始化,在函数体内赋值会报错

const int a:const 变量创建必须赋值,之后不能修改

int& r:引用定义时必须绑定实体,无法后期赋值

自定义类成员,没有无参默认构造函数**:**编译器无法自动初始化,必须手动进行传参初始化

以上三种要是在大括号内进行函数赋值就会立马报错。


  • 特点三:C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的
cpp 复制代码
class Test
{
    int x = 10; //声明默认值
public:
    Test(){}          //列表没写x,x=10
    Test():x(20){}    //列表写了x,优先20,默认10不起作用
};

初始化列表没手动初始化成员 : 使用类内默认值;

列表显式手动初始化 :类内缺省值失效。


推荐全都用初始化列表:

原因:首先 所有成员一定会经过初始化列表阶段;

对于C++当中的两种类型

  • 自定义类成员

列表没写 就 自动调用默认构造;无默认构造编译器直接报错;

  • 内置类型 (int/double/char)

类内给默认值 就 自动用默认值初始化;

没给默认、列表也不写 就是值随机(C++ 标准不强制初始化)

另外一个原因:

不用列表 ,先默认初始化、再函数体内覆盖赋值,效率低,还容易出现随机垃圾值,因此优先初始化列表。


初始化的顺序:

cpp 复制代码
class Test
{
    int a; //1先声明
    int b; //2后声明
public:
    Test()
:b(100),
 a(10) //列表先写b再a
    {}
};
//实际初始化顺序:先 a,后 b

初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆

关。建议声明顺序和初始化列表顺序保持⼀致。这里最爱挖坑了,我第一此也以为按照我声明的顺序。

记住各位老铁:不要用b(a)这种写法,这种写法顺序错乱会拿到随机值;

规范:初始化列表书写顺序 = 成员声明顺序


初始化列表总结:

⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;

⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要⾛初始化列表初始化;

图解:

为了加深大家的印象,练习一下:

下⾯程序的运⾏结果是什么(D)

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出 1 随机值

E. 输出 1 2

F. 输出 2 1

我理解就是三步走

1. 类里面声明成员(决定初始化先后顺序)

cpp 复制代码
private:
	int _a2=2; //先声明
	int _a1=2; //后声明

因为内存空间在这里开辟,初始化顺序永远按这个书写先后,和初始化列表无关。 没在初始化列表写的成员,先用类内 的默认值。

  1. 冒号初始化列表(初始化赋值) :_a1(a),_a2(_a1) 按照上面声明顺序依次初始化变量,列表赋值 > 类内默认值,默认值作废。 你原题: 先初始化_a2 (_a1) → _a1 是空随机 → _a2 = 乱码 再初始化_a1 (1) → _a1=1

  2. 构造函数大括号 {} 内部(覆盖赋值)最后执行,把前面初始化好的值直接覆盖改掉。

方便各位老铁记忆:

先声明定顺序 -->再列表初始化 -->最后大括号覆盖赋值


二.类型转换

  • C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  • 构造函数前⾯加explicit就不再⽀持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

C++ 隐式类型转换

代码演示:

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
    // 无explicit:单参数构造,支持int隐式转A类类型
    A(int a1)
        :_a1(a1)
    {}

    // 无explicit:C++11支持多参数列表隐式构造
    A(int a1, int a2)
        :_a1(a1)
        , _a2(a2)
    {}

    void Print()
    {
        cout << _a1 << " " << _a2 << endl;
    }

    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
public:
    // 参数为const A&,支持A类对象隐式转换为B
    B(const A& a)
        :_b(a.Get())
    {}
private:
    int _b = 0;
};

int main()
{
    A aa1 = 1;
    aa1.Print();
    const A& aa2 = 1;
    A aa3 = { 2,2 };
    B b = aa3;
    const B& rb = aa3;

    return 0;
}

进入代码分析阶段:

类中:

cpp 复制代码
A(int a1):
_a1(a1)
{
}

单参构造(无 explicit) 初始化列表把传入值给_a1;没初始化_a2 使用默认值 2,不加 explicit:允许 int 隐式转 AA aa1 =1合法)


cpp 复制代码
A(int a1,int a2)
:_a1(a1),
._a2(a2)
{
}

双参构造(无 explicit) 两个成员都在初始化列表赋值,类内默认值失效,C++11 支持A aa3={2,2}隐式列表转换


cpp 复制代码
void Print()、int Get() const

普通成员函数:打印数据、计算两数之和,const函数不修改成员。


cpp 复制代码
int _b = 0;

私有成员_b,默认初始 0。

cpp 复制代码
B(const A& a)
:_b(a.Get())
{
}

构造参数是const A&接收 A 对象,调用a.Get()求和赋值给_b。无 explicit:A 可以隐式转 BB b=aa3合法)


main函数:

cpp 复制代码
1.A aa1 = 1;

无 explicit 单参构造触发隐式转换:int 1 生成A(1)临时对象 ,编译器优化直接构造aa1 _a1=1_a2使用类内缺省值2,会输出结果:1 2


cpp 复制代码
​2. const A& aa2 = 1;

编译器构造临时A(1),临时对象生命周期直到至引用销毁,去掉const直接编译报错。


cpp 复制代码
3.A aa3 = { 2,2 };

C++11 新标准特性,多参数构造无 explicit,列表初始化隐式调用A(2,2)_a1=2,_a2=2


cpp 复制代码
4. B b = aa3;

B的构造参数是const A&,触发自定义类隐式转换,aa3隐式构造 B 对象,_b = 2+2=4


cpp 复制代码
5. const B& rb = aa3;

aa3 隐式生成 B 临时对象,临时对象绑定 const 引用。


explicit 禁用隐式转换

给构造加 explicit:

cpp 复制代码
explicit A(int a1):_a1(a1){}
explicit B(const A& a):_b(a.Get()){}

只能显式构造:

cpp 复制代码
A aa1(1);
A aa3(2,2);
B b(aa3);

explicit关键字:

修饰构造函数,禁止隐式类型转换,只允许显式调用构造

不加 explicit:编译器自动做隐式转换;

加 explicit:=和隐式写法全部报错。

总结:加了 explicit,等号隐式全不行,只能括号老老实实构造。


注意:

  • explicit 只修饰构造函数,别的函数不能用;
  • 只拦截隐式转换,不影响括号显式初始化;
  • 单参数构造默认全部加 explicit,避免无意间发生隐形类型转换的bug。

三.static成员

  • ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  • ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  • 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
  • ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  • 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表

代码演示:

cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
public:
    static int count; 
    int age = 18;    

    static void StaPrint()
    {
        cout << count << endl;
        //cout << age; 
    }
    void Print()
    {
        cout << age << " " << count << endl;
        StaPrint();
    }
};

int Person::count = 0;

int main()
{
 
    Person::count = 10;
    Person::StaPrint();

    Person p1,p2;
    p1.count++;
    cout << p2.count << endl;

    p1.Print();
    return 0;
}

开始一一讲解要点:

提前要点:普通成员:每个对象自己独有;

static 静态成员:整个类所有对象共用一份,独立存放

1**、静态成员变量:static 修饰成员变量,必须类外初始化、全对象共享、放静态区**

cpp 复制代码
class Person
{
public:
    static int count; // 只是声明,不开内存,告诉编译器有这个变量
    int age;
};
int Person::count = 0; // 类外定义+初始化,唯一开辟内存的地方

int age:每个对象单独一份,p1.agep2.age互不干扰,在对象内存里;

static int count:全类共用同一个变量,内存不在对象里,存在程序静态区,程序在运行一开始就开好内存,不管创建几个对象,永远只有一块 count。

cpp 复制代码
Person p1,p2;
p1.count++;
cout<<p2.count;//结果变成1,因为共用同一块数据

类里写static int count;只是声明,没开空间; 必须在类外面int Person::count=0;才算创建变量、分配内存。

2、静态成员函数:static 修饰函数,没有 this 指针

普通函数

普通成员函数void Show(){},编译器偷偷加参数void Show(Person* this)p1.Show()等价Show(&p1),this 存当前对象地址,用来找对象里的普通成员。

静态函数

static void StaShow(){}:没有隐藏的 this 指针,没有对象地址,不依附任何对象存在。

没有 this = xxx咱们不知道现在是 p1 还是 p2 在调用函数。

3.静态成员函数:只能访问静态成员,不能访问普通成员

cpp 复制代码
static void StaShow()
{
    cout<<count;//可以,静态变量全局共用,不用对象
    //cout<<age;//报错,age属于某个对象,需要this->age,否则静态函数没this找不到
}

静态函数不知道是哪个对象的 age,没有 this 拿不到每个对象独有的普通变量; 静态变量全类唯一,不用找对象,随便用。

4.非静态(普通)成员函数:静态、非静态全都能访问

cpp 复制代码
void Show()
{
    cout<<age;  //有this,访问自己对象的普通成员
    cout<<count;//静态成员全类共享,直接访问
    StaShow();  //调用静态函数
}

普通函数自带 this 指针,既能找到自己对象独有的数据,也能找到全类共用的静态数据。

5.静态成员两种访问方式:类名:: /或者对象.

静态不归对象管,没有创建对象也能访问

写法一:Person::count; Person::StaShow();(类名::,直接穿过类域找静态)

写法二:Person p; p.count; p.StaShow();(编译器自动转成类名访问,不通过对象内存取值)

cpp 复制代码
// 没创建任何对象,照样能用
cout<<Person::count;
Person::StaShow();

6.静态成员遵守访问限定符 public/protected/private

静态还是类的内部成员,对它的访问权限规则不变:

cpp 复制代码
class Person
{
private:
    static int count; //私有静态
};
int Person::count=0;
//main里 Person::count=10; 报错,私有外部不能访问

public:外面随便类名::访问

private/protected:只能类里面的函数访问,外部禁止直接访问

7.静态成员不能在类内写缺省值初始化(C++17 前)

cpp 复制代码
class Person
{
public:
    int age = 18; //普通成员可以类内缺省
    //static int count = 0; C++17之前报错
};

底层原理

int age=18:缺省值是构造函数初始化列表用的,创建对象进行构造的时候赋值;每个对象新建时初始化自己的 age;static count:不属于对象、不进行构造函数、不跟着对象创建初始化,不走初始化列表,所以不能用类内缺省赋值,只能类外初始化。

补充:C++17 新标准:inline static int count=0; 才允许类内初始化。


四.友元

定义:友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

通常情况下:类外面不能碰private/protected修饰的私有成员。 friend友元就是让类主动开门,让类外部函数 / 另一个整类,有权限访问自己的私有、保护成员。

语法规则:friend写在类内部,用来声明谁是我的友元。


  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  • 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
  • ⼀个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另⼀个类的友元函数,友元函数,都可以访问另⼀个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
  • 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
  • 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

各位老铁开始讲解:

友元类:

一个类 A 可以将另一个类 B 声明为自己的友元类, 方式就是在 A 中写 friend class B;

结果就是B 的所有成员函数都可以访问 A 的 privateprotected 成员。

友元类的三个特点:

特点一:

单向性:A 是 B 的友元,但 B 不一定 是 A 的友元

cpp 复制代码
#include <iostream>
using namespace std;

class B;  // 前面要声明

class A {
private:
    int secretA = 10;
    // A 没有声明 B 为友元
public:
    void showB(B& b);  // 声明
};

class B {
private:
    int secretB = 20;
    friend class A;    // 记住这是B 主动给 A 友元权限
};

// 结果就是A可以访问B的私有成员
void A::showB(B& b) {
    cout << "A 访问 B 的私有成员: " << b.secretB << endl;
}

int main() {
    A a;
    B b;
    a.showB(b);  // A 是 B 的友元

 
    // b 无法访问 a 的私有成员,因为 B 不是 A 的友元
    // cout << b.secretA;  // 报错

    return 0;
}

大致过程:先进行前置声明,告诉编译器有 B 这个类,A 里面用到 B &(传值会开多余的空间浪费内存哈) 不报错;我们看A 里面没写任何friend ,可以理解A 没有给 B 开门,B 没有办法访问 A 的私有secretA;接下来看B的类,B 把门打开,允许 A 全类所有成员函数访问自己的私有成员 secretB;A 的成员函数,靠着 B 给的友元权限,直接拿到 B 的私有变量secretB=20,代码正常运行。

a.showB(b):A 访问 B 私有没问题,B 提前授权 A 了。

b.secretA:B 想访问 A 的私有不行,A 没写 friend class B,没给 B 开门。

friend class A写在 B 里面 = A 是 B 友元

A 里无 friend = B 不是 A 友元

特点二:

不传递性:A 是 B 的友元,B 是 C 的友元,但 A 不是 C 的友元

cpp 复制代码
class C {
private:
    int secretC = 100;
    friend class B; //C只开门给B,只有B能访问C私有
};

class B {
private:
    int secretB = 200;
    friend class A; //B只开门给A,只有A能访问B私有
public:
    void showC(C& c) {
        cout << "B 访问 C: " << c.secretC << endl; //,B是C友元
    }
};

class A {
public:
    void showC(C& c) {
        //cout << c.secretC; //报错
        cout << "A 无法直接访问 C 的私有成员" << endl;
    }
};

大致过程:我们看到C 类里写friend class B,就是说 C 主动把自身私有成员的访问权限开放给 B,B 所有成员函数都能访问 C 的私有变量secretC;B 类里写friend class A,代表 B 给权限让 A 访问自己的私有成员secretB。从关系上看 A 是 B 的朋友、B 是 C 的朋友,但友元权限不会因为中间对象就自动自动传递,不一定A和C是朋友哈。简单理解就是:C 只给 B 开了家门,B 只给 A 开了家门,即便 A 能去 B 家里做客、B 能去 C 家里做客,C 没给 A 开门,A 就不能跟着 B 顺路蹭进 C 家里。

小细节: 函数参数采用引用C& c,不会拷贝新对象(不浪费空间),直接操作原本的 C 对象。所以 B 的showC依靠友元权限可以正常访问 C 私有;A 没有得到 C 的授权,在showC里直接访问secretC编译报错。

特点三:

友元类可以访问私有和保护成员

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class A {
private:
    int a = 5;
protected:
    string b = "保护数据";

    friend class B; // A把B设为友元
};

class B {
public:
    void print(A& tmp)
    {
        cout << "私有:" << tmp.a << endl;
        cout << "保护:" << tmp.b << endl;
    }
};

int main()
{
    A a1;
    B b1;
    b1.print(a1);
    return 0;
}

大致过程:我们看到A 里面私有成员a、保护成员b,正常外部不能访问;friend class B:A把B设成友元,A 主动给 B 开门,B 全部成员函数都是 A 的友元; print(A& tmp)是引用,tmp 就是 main 里 a1 的别名,没有拷贝新对象,不浪费空间; B 凭借友元权限,既能读取 A 的 private,也能读取 protected。

友元函数:

友元函数 老铁们理解就是是一个普通函数但不是类的成员函数哈,一般在类内部用 friend 关键字声明,它可以访问该类的 私有 和 保护 成员,而且声明位置不受访问限定符限制


友元函数特点:

特点一:

不是类的成员函数

cpp 复制代码
#include <iostream>
using namespace std;

class A {
private:
    int num = 100;

public:
    friend void print(A& o); //声明全局友元
    void show() {
        cout << "普通成员函数" << endl;
    }
};

//全局友元函数,不加A::
void print(A& a)
{
    cout << "友元取私有:" << o.num << endl;
}

int main()
{
    A a;
    print(a);
    // a.print(a); 这里就会报错,友元不是成员
    return 0;
}

大致内容:print全局函数,不属于 A 类 ,不能用对象.调用,直接print(a);A 里面写了 friend 权限,所以 print 能拿到 A 的私有 num;show是成员函数,只能a.show()调用。

特点二:

可以在类的任何地方声明

cpp 复制代码
#include <iostream>
using namespace std;

class A {
private:
    int x = 1;
    friend void f1();

protected:
    int y = 2;
    friend void f2();

public:
    int z = 3;
    friend void f3();
    friend void f4();
};

// 四个友元全都能访问私有、保护、公有
void f1() {
    A tmp;
    cout << tmp.x << tmp.y << tmp.z;
}
void f2() {
    A tmp;
    cout << tmp.x << tmp.y << tmp.z;
}
void f3() {
    A tmp;
    cout << tmp.x << tmp.y << tmp.z;
}
void f4() {
    A tmp;
    cout << tmp.x << tmp.y << tmp.z;
}

int main()
{
    f1();
    return 0;
}

大致内容:friend函数不管写在private、protected、public哪个位置,全部生效;

我们看到f1 写在私有区、f2 写在保护区、f3/f4 写在公有区,四个函数全是 A 的友元。只要是友元,就能随便拿:私有的x、保护的y、公有的z。但明白普通外部函数不能碰 x、y,只有被 friend 给权限才行。


五.内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。
cpp 复制代码
#include <iostream>
using namespace std;

class Out {    // 外部类
private:
    int num = 10;
    // 内部类,定义在外部类里面
    class In {
    public:
        void show(Out& a)
        {
            // 内部类默认是外部类友元,直接访问外部类私有
            cout << a.num << endl;
        }
    };
public:
    void test()
    {
        In in;
        in.show(*this);
    }
};

int main()
{
    Out a;
    a.test();
    // In in;  
    return 0;
}

通过上面的代码了解内部类的特点:

1.如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,只是受外部类类域和访问限定限制,外部类对象里不包含内部类。

对应到代码里就是:我们看到In写在Out里面就是内部类。In 是独立的类,Out 实例对象内存里没有存 In,不会跟着 Out 创建对象。全局类随便在哪创建,内部类受 Out 的public/private管控。

2、内部类默认是外部类的友元类。

对应到代码里:In的成员函数可以随便读取Outprivate私有成员num,不用手动写friend。 代码里show(Out& a)直接访问a.num就是靠这个特性。

3、内部类本质封装,A 只给 B 用就做成内部类;放 private/protected 就是专属内部类,类外无法使用。

class In写在 Out 的private里面:只有 Out 自己的成员函数test()能创建In对象;就连main 主函数不能定义In in;,外面彻底用不了,实现专属一个类的封装。

但不是没有办法哈:就是把内部类放 public,外部就能使用

cpp 复制代码
class Out {
public:
    class In{};
};
int main()
{
    Out::In in; //public内部类可以通过类域访问
}

六.匿名对象

  • ⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
  • 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
cpp 复制代码
#include <iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
	:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

class Solution {
public:
	int Sum_Solution(int n) {
		return n;
	}
};

int main()
{
	// 有名对象:起名字aa1,作用域到main结束才销毁
	A aa1;

	//A aa1(); 易错:编译器识别成函数声明,不是创建对象

	//匿名对象:没有变量名,
       //格式:类名(参数),生命周期仅限当前一行
	A();
	A(1);

	//有名对象aa2,全程可用
	A aa2(2);

	//匿名对象临时调用成员,用完立刻销毁
	Solution().Sum_Solution(10);

	return 0;
}

用 类型 (实参) 定义出来的对象叫做匿名对象;类型 对象名 (实参) 是有名对象,匿名对象生命周期只有当前一行

通过这个代码给大家解释一下:

我们看到,有名对象:A aa1;、A aa2 (2); 对象有名字 aa1、aa2,从创建开始,整个 main 函数执行完才析构释放,后面代码随时可以使用。匿名对象:A ();、A (1);、Solution ().Sum_Solution (10); 没有变量名字,只在当前这一行生效,这行代码跑完,马上调用析构销毁,下一行直接消失;A aa1();可能编译器当成函数声明(名叫 aa1、返回 A、无参),不会实例化对象。

生命周期这一块,大家记住,就一句话:起名 就能活到代码块末尾;不起名(类名 ())就是当场用完当场销毁。


七.对象拷⻉时的编译器优化

  • 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉(这里就提到我之前讲析构函数的时候为啥少几行了)。
  • 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化。
cpp 复制代码
#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=" << endl;
        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()
{
    A aa1;
    f1(aa1);    //普通传参,必拷贝
    cout << endl;

    f1(1);      //隐式构造+优化
    f1(A(2));   //匿名对象传参+优化
    cout << "******************8" << endl;

    f2();              
    A aa2 = f2();   
    aa1 = f2();    

    return 0;
}

我们看看这个代码哪里优化了?

A aa1; f1(aa1);

aa1 是现成有名对象,没法优化。值传参要复制一份给形参,必然调用拷贝构造,函数用完销毁副本。

f1(1);

没优化前:先用 1 造出临时对象,再把临时拷贝给形参。 编译器偷懒优化:直接拿数字 1 在形参位置原地构造,中间临时扔掉,少一次拷贝。

f1(A(2));

没优化前:先造匿名临时,再拷贝临时给形参。编译器优化后:匿名对象直接顶替形参,不再额外复制。这里简单小结一哈:传参只要是临时 / 字面量,编译器能就地构造就省拷贝;传入已有变量不能省。那咱打个比方理解一下:

  • 没优化:先单独造一个临时物件,再复制一份送去当形参。
  • 优化之后:临时物件直接占位当形参,省去复制。

f2();

无优化:函数内部造局部 aa,再拷贝一份变成返回临时,局部销毁。 编译器优化后:不开辟局部变量,直接在外面临时空间构造对象,只构造 1 次,无拷贝。

A aa2 = f2();

看一下不优化啥情况f2函数里面:造局部对象 aa构造 1 次return aa:把局部 aa拷贝成外面临时对象拷贝构造 1 次,函数里局部 aa 销毁;临时对象再拷贝给 aa2又一次拷贝构造。一共:1 构造 + 2 次拷贝。

编译器优化

编译器清楚:最后这个数据就是要放到aa2的内存里。 直接不去函数内部新开空间造局部 aa,直接跑到外面 aa2 的地盘就地构造对象。 只执行1 次普通构造,没有任何拷贝,省去两次拷贝。

如果老铁还不明白:打个比方:

  • 未优化:厨房做一份饭(局部 aa),打包一份送到楼道临时桌(临时对象)→再从临时桌搬到你餐桌 aa2。搬两次 = 两次拷贝。
  • 优化:厨师直接在你餐桌上做饭,做完就是你的,不用来回搬运。

aa1 = f2();

f2 依旧编译器优化后:内部直接生成返回临时。

但是=(等号)赋值不能优化省略:已经存在 aa1 对象,只能调用赋值运算符把临时数据赋值过去。

各位老铁就可以理解为:编译器规则:就是咱能合并空间就合并,能少拷贝就少拷贝;但是赋值运算没法合并,必须正常调用重载。

我用的就是VS,2022。Debug(VS2019):优化保守,部分拷贝保留;Debug(VS2022)/Release:优化,绝大多数拷贝全被编译器干掉。


到这里我们再次理解一下类和对象

计算机只认识二进制数据(0和1),不认识现实中的实体。要让计算机认识这些实体,程序员需要用面向对象语言(比如C++)定义类------类就是对某一类实体的描述,包括它的属性(叫什么、多大)和行为(能做什么)。

定义完类,就创建了一个新的数据类型。然后通过这个类型去实例化对象,计算机才能真的处理这些实体。

我们可以这样理解:计算机很笨,它只认识 0 和 1。你想要计算机处理一个"人"或者"汽车",它根本不认识这些东西。

所以你需要用代码来告诉计算机:"计算机你看好了,我定义一个'人'这个东西,他有名字、年龄,他能吃饭、能走路。以后我说的'人',就是这个样子。"这个"告诉计算机的过程",就是定义类

然后你让计算机根据这个"图纸"造一个具体的人出来,比如"张飞,18岁",这就是创建对象

一句话概括就是:类就是"说明书",对象就是"产品"。计算机看不懂实物,但看得懂你写的说明书。


各位老铁,本节内容就讲完了,咱们类和对象也都是阐述完毕,希望给大家带来新的收获,完结撒花!下节内容见

相关推荐
稷下元歌1 小时前
7天学会plc加机器视觉关于运动控制部份,配套视频在bib
开发语言·c++·git·vscode·python·docker·pip
薇茗1 小时前
【C++】 类与对象 基础篇
开发语言·c++·基础语法·类与对象
晚笙coding1 小时前
从零讲透 LangChain 输出格式化:让模型真的“能用”
java·开发语言·langchain
奋斗的小方1 小时前
Java进阶篇1-1:异常
java·开发语言·python
A_humble_scholar1 小时前
Linux(三)深入理解 Makefile:自动变量、增量编译原理与文件时间属性
linux·服务器·c++·makefile
sycmancia1 小时前
Qt——多页面切换组件
开发语言·qt
思麟呀1 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
Full Stack Developme1 小时前
Hutool CollUtil 教程
java·开发语言·windows·python
落羽的落羽1 小时前
【项目】JsonRpc框架——开发实现2(业务层)
linux·数据结构·c++·人工智能·算法·json·动态规划