深入理解C++系列(04)——类和对象(下)

文章目录

  • [C++ 类和对象(下)复习总结](#C++ 类和对象(下)复习总结)
    • 一、本讲核心主线
    • 二、再探构造函数:初始化列表
    • [1. 构造函数体赋值 vs 初始化列表](#1. 构造函数体赋值 vs 初始化列表)
    • [2. 初始化列表的本质](#2. 初始化列表的本质)
    • [3. 每个成员变量只能在初始化列表中出现一次](#3. 每个成员变量只能在初始化列表中出现一次)
    • [4. 必须使用初始化列表的成员](#4. 必须使用初始化列表的成员)
    • [5. 为什么引用和 const 必须初始化?](#5. 为什么引用和 const 必须初始化?)
    • [6. C++11 支持成员变量声明处给缺省值](#6. C++11 支持成员变量声明处给缺省值)
    • [7. 即使不写初始化列表,成员也会走初始化列表](#7. 即使不写初始化列表,成员也会走初始化列表)
    • [8. 初始化顺序由成员声明顺序决定(常考)](#8. 初始化顺序由成员声明顺序决定(常考))
    • [9. 初始化列表使用建议](#9. 初始化列表使用建议)
    • 三、类型转换
    • [1. 内置类型可以隐式转换为类类型](#1. 内置类型可以隐式转换为类类型)
    • [2. const 引用可以绑定转换出的临时对象(并且只能用const)](#2. const 引用可以绑定转换出的临时对象(并且只能用const))
    • [3. C++11 支持多参数隐式转换](#3. C++11 支持多参数隐式转换)
    • [4. 类类型之间也可以隐式转换](#4. 类类型之间也可以隐式转换)
    • [5. explicit 禁止隐式类型转换](#5. explicit 禁止隐式类型转换)
    • [四、static 成员](#四、static 成员)
    • [1. 静态成员变量](#1. 静态成员变量)
    • [2. 静态成员变量不走构造函数初始化列表](#2. 静态成员变量不走构造函数初始化列表)
    • [3. 静态成员函数](#3. 静态成员函数)
    • [4. 静态成员的访问方式](#4. 静态成员的访问方式)
    • [5. 静态成员也受访问限定符限制](#5. 静态成员也受访问限定符限制)
    • [6. 用 static 统计对象数量](#6. 用 static 统计对象数量)
    • [7. 牛客题:求 1+2+...+n](#7. 牛客题:求 1+2+...+n)
    • 五、友元
    • [1. 什么是友元?](#1. 什么是友元?)
    • [2. 友元函数](#2. 友元函数)
    • [3. 友元函数声明位置不受访问限定符限制](#3. 友元函数声明位置不受访问限定符限制)
    • [4. 一个函数可以是多个类的友元](#4. 一个函数可以是多个类的友元)
    • [5. 友元类](#5. 友元类)
    • [6. 友元关系的特点](#6. 友元关系的特点)
    • 六、内部类
    • [1. 什么是内部类?](#1. 什么是内部类?)
    • [2. 内部类是独立的类](#2. 内部类是独立的类)
    • [3. 内部类受外部类类域限制](#3. 内部类受外部类类域限制)
    • [4. 内部类默认是外部类的友元类](#4. 内部类默认是外部类的友元类)
    • [5. 内部类的使用场景](#5. 内部类的使用场景)
    • [6. 用内部类改写求和题](#6. 用内部类改写求和题)
    • 七、匿名对象
    • [1. 什么是匿名对象?](#1. 什么是匿名对象?)
    • [2. 匿名对象生命周期(当前行)](#2. 匿名对象生命周期(当前行))
    • [3. 匿名对象适合临时用一下](#3. 匿名对象适合临时用一下)
    • [4. 匿名对象和函数声明易混点](#4. 匿名对象和函数声明易混点)
    • 八、对象拷贝时的编译器优化
    • [1. 为什么会有拷贝优化?](#1. 为什么会有拷贝优化?)
    • [2. 优化规则不是完全固定的](#2. 优化规则不是完全固定的)
    • [3. 传值传参场景](#3. 传值传参场景)
    • [4. 隐式类型转换 + 传值传参可能被优化](#4. 隐式类型转换 + 传值传参可能被优化)
    • [5. 匿名对象传参也可能被优化](#5. 匿名对象传参也可能被优化)
    • [6. 传值返回场景](#6. 传值返回场景)
    • [7. 接收返回值时的优化](#7. 接收返回值时的优化)
    • [8. 赋值场景通常难以完全消除赋值](#8. 赋值场景通常难以完全消除赋值)
    • [9. 如何看待拷贝优化?](#9. 如何看待拷贝优化?)
    • 九、知识点汇总
      • [知识点 1:必须使用初始化列表的成员](#知识点 1:必须使用初始化列表的成员)
      • [知识点 2:初始化顺序规则](#知识点 2:初始化顺序规则)
      • [知识点 3:成员缺省值作用](#知识点 3:成员缺省值作用)
      • [知识点 4:`explicit` 的作用](#知识点 4:explicit 的作用)
      • [知识点 5:静态成员变量初始化规则](#知识点 5:静态成员变量初始化规则)
      • [知识点 6:静态成员函数特点](#知识点 6:静态成员函数特点)
      • [知识点 7:友元关系特点](#知识点 7:友元关系特点)
      • [知识点 8:内部类本质](#知识点 8:内部类本质)
    • 十一、总结与回顾
    • 下期预告
    • 结语

◆ 博主: @此生决int
分享编程知识!深挖底层原理!持续原创更新!

热门专栏:

深入理解 C++系列专栏|快速复习系列专栏
算法系列专栏

📌 本系列知识点前后关联较强,建议按照专栏顺序阅读哦

本系列主要面向的阅读人群

1,学完 C 语言,准备系统学习 C++ 的同学

2,已经学习过 C++,希望重新梳理知识体系的同学

3,想深入理解 C++ 设计思想与底层原理,而不仅仅停留在语法层面的同学

不止教你会写代码,更带你理解代码为什么这样设计!

C++ 类和对象(下)复习总结

一、本讲核心主线

这一讲继续深入类和对象,主要解决几个问题:

  1. 构造函数除了函数体赋值,还有什么更规范的初始化方式?
  2. 为什么有些成员必须放到初始化列表?
  3. 内置类型能不能隐式转换成类类型?如何禁止?
  4. static 成员变量和成员函数到底属于谁?
  5. 友元为什么能访问私有成员?它会不会破坏封装?
  6. 内部类和外部类是什么关系?
  7. 匿名对象生命周期有多长?
  8. 对象拷贝时编译器会做哪些优化?

一句话概括:

类和对象(下)重点是在对象初始化、类级别成员、封装边界和对象临时性行为上继续加深理解


二、再探构造函数:初始化列表

1. 构造函数体赋值 vs 初始化列表

之前写构造函数时,经常在函数体内给成员变量赋值:

cpp 复制代码
Date(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

但更规范的初始化方式是初始化列表:

cpp 复制代码
Date(int year, int month, int day)
    : _year(year)
    , _month(month)
    , _day(day)
{
}

初始化列表格式:

cpp 复制代码
构造函数名(参数列表)
    : 成员1(初始值)
    , 成员2(初始值)
    , 成员3(初始值)
{
    // 构造函数体
}

2. 初始化列表的本质

初始化列表可以理解为:

成员变量真正定义初始化的地方

函数体内赋值并不是初始化,而是:

成员已经初始化完成后,再进行赋值

这点非常重要。或许你还是有疑问,他们到底有什么区别?注释1

例如:

cpp 复制代码
Date(int year)
    : _year(year) // 初始化
{
    _month = 1;  // 赋值
}

注释1

初始化就是创建对象并赋值,赋值就是赋值,所以,初始化列表就是让对象成员在创建的同时进行初始化,那这有什么意义呢?先创建对象再进行初始化不是也可以吗?一般情况是这样的,但是对于一些必须在创建的时候就进行初始化的成员来说,就必须到初始化列表,例如:

  • 1,const修饰的变量,
    (const 变量只能初始化一次
    不能赋值)
  • 2,引用变量
    (引用必须定义时初始化)
  • 3,没有默认构造的成员对象
    (因为要初始化就要先创建对象,但是他没有默认构造函数)

3. 每个成员变量只能在初始化列表中出现一次

错误:

cpp 复制代码
Date(int year)
    : _year(year)
    , _year(2024)
{
}

一个成员不能被初始化两次。


4. 必须使用初始化列表的成员

下面三类成员必须放在初始化列表中初始化:

1. 引用成员变量
2. const 成员变量
3. 没有默认构造函数的自定义类型成员变量

示例:

cpp 复制代码
class Time
{
public:
    Time(int hour)
        : _hour(hour)
    {}

private:
    int _hour;
};

class Date
{
public:
    Date(int& x, int year = 1, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
        , _t(12)
        , _ref(x)
        , _n(1)
    {}

private:
    int _year;
    int _month;
    int _day;
    Time _t;      // 没有默认构造
    int& _ref;    // 引用
    const int _n; // const
};

如果不放初始化列表,会报错:

  • Time 没有默认构造函数。
  • 引用成员必须初始化。
  • const 成员必须初始化。

5. 为什么引用和 const 必须初始化?

引用成员

引用定义时必须初始化,且不能重新绑定:

cpp 复制代码
int& _ref;

所以只能在初始化列表中完成初始化。


const 成员

const 成员一旦初始化后不能修改:

cpp 复制代码
const int _n;

如果不在初始化阶段给值,进入函数体后就不能再赋值了。


没有默认构造的自定义类型成员

cpp 复制代码
Time _t;

如果 Time 只有:

cpp 复制代码
Time(int hour);

没有:

cpp 复制代码
Time();

那么编译器没办法默认构造 _t,必须在初始化列表中显式传参:

cpp 复制代码
_t(12)

6. C++11 支持成员变量声明处给缺省值

示例:

cpp 复制代码
class Date
{
private:
    int _year = 1;
    int _month = 1;
    int _day;
    Time _t = 1;
    const int _n = 1;
    int* _ptr = (int*)malloc(12);
};

注意:

这里不是初始化,而是给初始化列表提供缺省值。

如果某个成员没有在初始化列表中显式初始化,就会使用声明处给的缺省值。

例如:

cpp 复制代码
Date()
    : _month(2)
{
}

则:

  • _month 使用初始化列表中的 2
  • _year 使用声明处缺省值 1
  • _t 使用声明处缺省值 1 构造。
  • _n 使用声明处缺省值 1
  • _day 如果没有缺省值,则是否初始化不确定。

7. 即使不写初始化列表,成员也会走初始化列表

这里的总结非常重要:

无论是否显式写初始化列表,每个构造函数都有 初始化列表。

无论是否在初始化列表中显式初始化成员变量,每个成员变量都要走初始化列表初始化。

也就是说:

cpp 复制代码
Date()
{
}

并不代表成员不初始化,而是它们仍然经过默认初始化逻辑。


8. 初始化顺序由成员声明顺序决定(常考)

这是本节最容易考的点之一。

初始化列表中成员的书写顺序不决定真正初始化顺序。

真正顺序 由类中成员声明顺序决定。

示例:

cpp 复制代码
class A
{
public:
    A(int a)
        : _a1(a)
        , _a2(_a1)
    {}

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

private:
    int _a2 = 2;
    int _a1 = 2;
};

虽然初始化列表写的是:

cpp 复制代码
_a1(a), _a2(_a1)

但成员声明顺序是:

cpp 复制代码
int _a2 = 2;
int _a1 = 2;

所以实际先初始化 _a2,再初始化 _a1

此时 _a2(_a1) 中的 _a1 还没有初始化,结果可能是随机值。

因此运行结果不是直觉中的 1 1

结论:

成员初始化顺序只看声明顺序,不看初始化列表顺序。建议声明顺序和初始化列表顺序保持一致。


9. 初始化列表使用建议

建议:尽量用初始化列表初始化

尽量使用初始化列表初始化成员变量。

原因:

  1. 有些成员必须用初始化列表。
  2. 初始化列表比函数体赋值更符合对象初始化语义。
  3. 对自定义类型成员来说,初始化列表可以避免"先默认构造再赋值"的额外成本。
  4. 代码风格更统一。

三、类型转换

1. 内置类型可以隐式转换为类类型

如果一个类有以某个内置类型为参数的构造函数,那么 C++ 支持从该内置类型隐式转换为类类型对象。

示例:

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

private:
    int _a1 = 1;
    int _a2 = 2;
};

可以写:

cpp 复制代码
A aa1 = 1;

本质:

  1. 1 构造一个 A 的临时对象。
  2. 再用临时对象拷贝构造 aa1
  3. 编译器通常会优化为直接构造。

2. const 引用可以绑定转换出的临时对象(并且只能用const)

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

本质:

  • 1 先隐式转换成 A 临时对象。
  • const A& 绑定这个临时对象。

这和前面学过的 const 引用绑定临时对象是一致的。


3. C++11 支持多参数隐式转换

例如:

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

C++11 后可以写:

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

4. 类类型之间也可以隐式转换

示例:

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

private:
    int _b = 0;
};

如果 B 有接收 A 的构造函数,就可以写:

cpp 复制代码
A aa3 = { 2, 2 };
B b = aa3;//隐式转换后调用构造函数进行初始化。
const B& rb = aa3;

注释1:

cpp 复制代码
const B& rb = aa3;
≈
B temp(aa3);
const B& rb = temp;

即,aa3先隐私类型转换为B类型,即创建一个临时变量tmp,tmp是用aa3进行初始化,正常情况下临时变量这一行就会销毁,但是,const引用可以延长临时对象生命周期,所以,它没有立即销毁,

注意点:

  • temp 是编译器生成的临时 B 对象;
  • rb 是 temp 的别名;
  • rb 不是 aa3 的别名;
  • const 的作用是允许绑定临时对象,并延长其生命周期

本质:

通过构造函数把 A 对象转换成 B 对象。


5. explicit 禁止隐式类型转换

如果不希望构造函数支持隐式转换,可以在构造函数前加 explicit

cpp 复制代码
class A
{
public:
    explicit A(int a1)
        : _a1(a1)
    {}
};

此时不允许:

cpp 复制代码
A aa1 = 1; // 错误

但可以显式构造:

cpp 复制代码
A aa1(1);
A aa2 = A(1);

结论:

单参数构造函数容易触发隐式类型转换,不希望发生时加 explicit


四、static 成员

1. 静态成员变量

static 修饰的成员变量叫静态成员变量。

cpp 复制代码
class A
{
private:
    static int _scount;
};

特点:

1. 静态成员变量属于整个类,不属于某个具体对象。
2. 所有对象共享同一份静态成员变量。
3. 静态成员变量不存储在对象中。
4. 静态成员变量存放在静态区。
5. 静态成员变量一定要在类外定义初始化。

类内声明:

cpp 复制代码
static int _scount;

类外定义初始化:

cpp 复制代码
int A::_scount = 0;

2. 静态成员变量不走构造函数初始化列表

静态成员变量不属于某个对象,因此:

它不会进入构造函数初始化列表。

所以不能像普通成员那样依赖声明处缺省值给构造函数用。

在此强调:

静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表用的,而静态成员不走初始化列表。

注意:现代 C++ 对 static const 整型、inline static 有更多规则,但按本课当前阶段记这些结论即可。


3. 静态成员函数

static 修饰的成员函数叫静态成员函数。

cpp 复制代码
static int GetACount()
{
    return _scount;
}

特点:

  1. 静态成员函数没有 this 指针。
  2. 静态成员函数可以访问静态成员变量和静态成员函数。
  3. 静态成员函数不能直接访问非静态成员变量
  4. 非静态成员函数可以访问静态成员和非静态成员。

为什么静态成员函数不能访问非静态成员?

因为非静态成员属于具体对象,访问它们需要 this 指针;而静态成员函数没有 this


4. 静态成员的访问方式

静态成员可以通过类名访问:

cpp 复制代码
A::GetACount();

也可以通过对象访问:

cpp 复制代码
a1.GetACount();

但更推荐用类名访问,因为静态成员本来就属于类。


5. 静态成员也受访问限定符限制

例如:

cpp 复制代码
class A
{
private:
    static int _scount;
};

外部不能直接访问:

cpp 复制代码
cout << A::_scount << endl; // 错误,private

可以提供公共静态函数访问:

cpp 复制代码
static int GetACount()
{
    return _scount;
}

6. 用 static 统计对象数量

cpp 复制代码
class A
{
public:
    A()
    {
        ++_scount;
    }

    A(const A& t)
    {
        ++_scount;
    }

    ~A()
    {
        --_scount;
    }

    static int GetACount()
    {
        return _scount;
    }

private:
    static int _scount;
};

int A::_scount = 0;

逻辑:

  • 构造对象:数量加一。
  • 拷贝构造对象:数量加一。
  • 析构对象:数量减一。
  • 静态成员变量 _scount 记录当前对象数量。

7. 牛客题:求 1+2+...+n

利用静态成员变量和对象数组:

cpp 复制代码
class Sum
{
public:
    Sum()
    {
        _ret += _i;
        ++_i;
    }

    static int GetRet()
    {
        return _ret;
    }

private:
    static int _i;
    static int _ret;
};

int Sum::_i = 1;//静态变量在类外进行初始化,如果没写,就会出现链接错误!
int Sum::_ret = 0;

调用:

cpp 复制代码
Sum arr[n];
return Sum::GetRet();

创建 n 个对象,每次构造累加一次。

注意:

这个题利用的是构造函数自动调用和静态成员共享状态。


五、友元

1. 什么是友元?

友元提供了一种突破类访问限定符的方式

友元分为:

  1. 友元函数
  2. 友元类

声明方式是在类内部写:

cpp 复制代码
friend 函数声明;
friend class 类名;

友元可以访问类的 privateprotected 成员。


2. 友元函数

作用:友元函数不是成员函数 ,但拥有访问类私有成员和保护成员的权限

示例:

cpp 复制代码
class B;//这个是不是写错了?

class A
{
    friend void func(const A& aa, const B& bb);

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
    friend void func(const A& aa, const B& bb);

private:
    int _b1 = 3;
    int _b2 = 4;
};

void func(const A& aa, const B& bb)
{
    cout << aa._a1 << endl;
    cout << bb._b1 << endl;
}

注意:

  • func 可以访问 AB 的私有成员。
  • func 不是 AB 的成员函数。
  • 它只是被声明为友元的普通函数。

3. 友元函数声明位置不受访问限定符限制

友元声明写在:

cpp 复制代码
public:
private:
protected:

任何位置都可以。

因为友元声明不是类成员,不受访问限定符限制。


4. 一个函数可以是多个类的友元

如上例:

cpp 复制代码
friend void func(const A& aa, const B& bb);

func 同时是 AB 的友元函数,因此可以同时访问两者的私有成员。


5. 友元类

示例:

cpp 复制代码
class A
{
    friend class B;

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
public:
    void func1(const A& aa)
    {
        cout << aa._a1 << endl;
    }

    void func2(const A& aa)
    {
        cout << aa._a2 << endl;
    }
};

含义:

BA 的友元类,所以 B 的所有成员函数都可以访问 A 的私有和保护成员。


6. 友元关系的特点

特点一:友元关系是单向的

如果:

cpp 复制代码
class A
{
    friend class B;
};

表示:

B 可以访问 A 的私有成员。

但不表示:

A 可以访问 B 的私有成员。


特点二:友元关系不具有传递性

如果:

text 复制代码
A 是 B 的友元
B 是 C 的友元

不能推出:

text 复制代码
A 是 C 的友元

特点三:友元会破坏封装

友元确实方便,但它允许外部函数或其他类访问私有成员,会增加耦合度。

结论:

友元不宜多用,只在确实能明显简化设计时使用。

典型合理场景:

cpp 复制代码
operator<<
operator>>

因为它们通常需要作为全局函数访问类的私有成员。


六、内部类

1. 什么是内部类?

如果一个类定义在另一个类内部,这个类叫内部类。

cpp 复制代码
class A
{
public:
    class B
    {
    public:
        void foo(const A& a)
        {}
    };
};

这里 BA 的内部类。


2. 内部类是独立的类

重点:

内部类是一个独立的类,不是外部类对象的一部分。

例如:

cpp 复制代码
class A
{
public:
    class B
    {
        int _b1;
    };

private:
    int _h = 1;
};

sizeof(A) 不会因为 B 中有 _b1 就增加。

因为:

外部类对象中不包含内部类对象


3. 内部类受外部类类域限制

如果 BA 的内部类,外部使用时要写:

cpp 复制代码
A::B b;

如果 B 放在 privateprotected 区域,则类外不能直接使用。


4. 内部类默认是外部类的友元类

重点:

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

因此内部类可以访问外部类的私有成员。

示例:

cpp 复制代码
class A
{
private:
    static int _k;
    int _h = 1;

public:
    class B
    {
    public:
        void foo(const A& a)
        {
            cout << _k << endl;    // 可以访问静态私有成员
            cout << a._h << endl;  // 可以通过对象访问非静态私有成员
        }
    };
};

注意:

  • 访问静态成员 _k 不需要对象。
  • 访问非静态成员 _h 需要通过外部类对象 a

5. 内部类的使用场景

当一个类主要服务于另一个类,并且二者关系非常紧密时,可以考虑内部类。

例如:

text 复制代码
A 类实现出来主要就是给 B 类内部使用

可以把 A 设计成 B 的内部类。

如果放到 private 区域,就变成 B 的专属内部实现,外部无法直接使用。

这也是一种封装。


6. 用内部类改写求和题

cpp 复制代码
class Solution
{
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };

    static int _i;
    static int _ret;

public:
    int Sum_Solution(int n)
    {
        Sum arr[n];
        return _ret;
    }
};

这里 Sum 只服务于 Solution,放成内部类更封装。


七、匿名对象

1. 什么是匿名对象?

普通有名对象:

cpp 复制代码
A aa1;
A aa2(2);

匿名对象:

cpp 复制代码
A();
A(1);

定义方式:

cpp 复制代码
类型(实参)

没有对象名,所以叫匿名对象。


2. 匿名对象生命周期(当前行)

匿名对象生命周期:

只在当前这一行。

示例:

cpp 复制代码
A();
A(1);
A aa2(2);

A();A(1); 创建出的匿名对象,在当前语句结束后立即析构。


3. 匿名对象适合临时用一下

例如:

cpp 复制代码
Solution().Sum_Solution(10);

含义:

  1. 临时构造一个 Solution 匿名对象。
  2. 调用它的 Sum_Solution(10)
  3. 当前语句结束后匿名对象销毁。

适合场景:

只需要临时调用一次成员函数,不想专门定义一个对象名。


4. 匿名对象和函数声明易混点

不能这样定义无参对象:(之前反复说过)

cpp 复制代码
A aa1();

这会被理解为函数声明。

但匿名对象可以这样写:

cpp 复制代码
A();

因为它没有对象名,不会被解释成函数声明。


八、对象拷贝时的编译器优化

1. 为什么会有拷贝优化?

对象传参和返回值过程中可能产生很多临时对象和拷贝构造。

现代编译器为了提高效率,会在不影响程序正确性的前提下:

尽可能减少不必要的拷贝。


2. 优化规则不是完全固定的

这里强调:

C++ 标准没有严格规定所有优化方式,不同编译器会根据情况自行处理。

不同环境可能看到不同输出:

  • VS2019 Debug
  • VS2022 Debug
  • g++
  • clang++

都可能不完全一样。

在 Linux 下可以用:

bash 复制代码
g++ test.cpp -fno-elide-constructors

关闭构造相关优化,观察原始拷贝过程。


3. 传值传参场景

示例:

cpp 复制代码
void f1(A aa)
{}

调用:

cpp 复制代码
A aa1;
f1(aa1);

一般语义:

  • aa1 已经存在。
  • 传值给形参 aa
  • 需要调用拷贝构造。

4. 隐式类型转换 + 传值传参可能被优化

cpp 复制代码
f1(1);

语义上可能是:

  1. 1 构造一个临时 A 对象。
  2. 再拷贝构造形参 aa

但编译器通常会把:

text 复制代码
构造 + 拷贝构造

优化为:

text 复制代码
直接构造形参

5. 匿名对象传参也可能被优化

cpp 复制代码
f1(A(2));

语义上:

  1. 创建匿名对象 A(2)
  2. 拷贝构造形参 aa

编译器可能优化为直接构造形参。


6. 传值返回场景

cpp 复制代码
A f2()
{
    A aa;
    return aa;
}

语义上:

  • 函数内部构造局部对象 aa
  • 返回时可能拷贝构造返回临时对象。

但现代编译器可能进行返回值优化,把局部对象和返回对象合并。


7. 接收返回值时的优化

cpp 复制代码
A aa2 = f2();

语义上可能涉及:

  1. f2 内构造局部对象。
  2. 拷贝构造返回临时对象。
  3. 用返回临时对象拷贝构造 aa2

编译器可能把多次拷贝合并为更少的构造,甚至直接构造 aa2


8. 赋值场景通常难以完全消除赋值

cpp 复制代码
aa1 = f2();

这里 aa1 已经存在。

即使返回值过程可以优化,最终仍然要把返回结果赋给 aa1,所以赋值运算符重载通常仍会参与。


9. 如何看待拷贝优化?

重点不是死背某个编译器打印几次构造、几次拷贝,而是理解:

  1. 从语义上看,哪些地方可能调用构造、拷贝构造、赋值。
  2. 从实际运行看,编译器可能优化掉一些临时对象。
  3. 不同编译器、不同模式下输出可能不同。

结论:

学习时先按语义理解调用过程,再知道编译器可能优化,不要用某一次打印结果反推语言规则。


九、知识点汇总

知识点 1:必须使用初始化列表的成员

cpp 复制代码
int& _ref;
const int _n;
Time _t; // Time 没有默认构造

这些成员必须放初始化列表。


知识点 2:初始化顺序规则

初始化顺序只看成员声明顺序,不看初始化列表顺序。


知识点 3:成员缺省值作用

cpp 复制代码
int _year = 1;

它是给初始化列表提供缺省值。


知识点 4:explicit 的作用

用于禁止单参数构造函数的隐式类型转换。


知识点 5:静态成员变量初始化规则

类内声明:

cpp 复制代码
static int _scount;

类外定义:

cpp 复制代码
int A::_scount = 0;

知识点 6:静态成员函数特点

静态成员函数没有 this 指针。


知识点 7:友元关系特点

友元关系:

  • 单向
  • 不传递

知识点 8:内部类本质

内部类是定义在类域中的独立类。


十一、总结与回顾

  • 初始化列表是成员真正初始化的地方,构造函数体内是赋值。
  • 引用成员、const 成员、无默认构造的自定义类型成员必须放初始化列表。
  • 每个构造函数都有初始化列表,每个成员都会走初始化列表。
  • 成员初始化顺序只看声明顺序,不看初始化列表书写顺序。
  • C++11 成员声明处的值是给初始化列表使用的缺省值。
  • 单参数构造函数可能导致隐式类型转换,不想允许就加 explicit
  • 静态成员变量属于类,所有对象共享,不存在对象中,类外初始化。
  • 静态成员函数没有 this,不能直接访问非静态成员。
  • 友元可以访问私有成员,但不是类成员,会破坏封装,不宜滥用。
  • 友元关系单向、不传递。
  • 内部类是独立类,不占外部类对象空间,默认是外部类友元。
  • 匿名对象生命周期只在当前语句。
  • 编译器会优化对象拷贝,打印次数可能因编译器不同而不同。

下期预告


结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  也欢迎订阅我的**

深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++

算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线

快速复习系列:知识梳理、查漏补缺,考前冲刺必备

Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
Yeauty1 小时前
你那条 ffmpeg 命令,一键翻成 Rust builder 代码
开发语言·rust·ffmpeg
鱼香鱼香rose1 小时前
java-2
java·开发语言·python
星栈独行11 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
开发语言·javascript·人工智能·程序人生
2601_9545267511 小时前
【工业传感与算法实战】温漂补偿与零点抗漂破局:基于二阶多项式拟合的 C/C++ 边缘校准算法,深度拆解“压力变送器什么牌子好”的技术硬指标
c语言·c++·算法
qq_4480111611 小时前
C语言中的变量和函数的定义与声明
android·c语言·开发语言
code_pgf12 小时前
`unordered_map` 详解
c++
孫治AllenSun13 小时前
【DataX】生产环境搭建DataX集群案例
java·开发语言·jvm
一拳一个呆瓜13 小时前
【STL】iostream 编程:检测提取操作产生的错误
c++·stl
稚南城才子,乌衣巷风流13 小时前
判断素数与拓展应用
c++