C++ 类与对象的初始化机制、成员访问边界与拷贝优化


C++ 类与对象的初始化机制、成员访问边界与拷贝优化

  • [C++ 类与对象的初始化机制、成员访问边界与拷贝优化](#C++ 类与对象的初始化机制、成员访问边界与拷贝优化)
  • [1. 阅读前问题卡:C++ 类与对象](#1. 阅读前问题卡:C++ 类与对象)
    • [1.1 阅读前先看这几个问题](#1.1 阅读前先看这几个问题)
    • [1.2 读完后完成这 3 道高频问题](#1.2 读完后完成这 3 道高频问题)
    • [1.3 自检清单](#1.3 自检清单)
  • [2. 前言](#2. 前言)
    • [2.1 对象从哪些入口建立](#2.1 对象从哪些入口建立)
    • [2.2 多个对象共用一份记录](#2.2 多个对象共用一份记录)
    • [2.3 访问许可与内部归属](#2.3 访问许可与内部归属)
    • [2.4 临时使用与减少中间交接](#2.4 临时使用与减少中间交接)
  • [3. 再探构造函数](#3. 再探构造函数)
  • [4. 类型转换](#4. 类型转换)
  • [5. static 成员](#5. static 成员)
  • [6. 友元](#6. 友元)
  • [7. 内部类](#7. 内部类)
  • [8. 匿名对象](#8. 匿名对象)
  • [9. 对象拷贝时的编译器优化](#9. 对象拷贝时的编译器优化)

1. 阅读前问题卡:C++ 类与对象

建议先读:第 3 节的初始化列表规则,再对照两个 Date 示例和成员初始化顺序题。


1.1 阅读前先看这几个问题

  1. 初始化列表与构造函数体内赋值有什么区别?哪些成员必须在初始化列表中初始化?
  2. 成员声明处的缺省值何时生效?实际初始化顺序由声明顺序还是初始化列表顺序决定?
  3. 哪些构造函数支持隐式类型转换?加上 explicit 后,文中的转换写法会受到什么影响?
  4. 为什么静态成员能用于对象计数和求和?静态成员函数没有 this 指针,会限制它访问哪些成员?
  5. 友元函数与成员函数有什么区别?友元类关系为什么不能反向使用或传递?
  6. 内部类的独立性、访问权限与外部类对象的组成有什么关系?求和示例为何把 Sum 放进 Solution 内部?
  7. 匿名对象与有名对象在写法和生命周期上有什么差别?Solution().Sum_Solution(10) 展示了哪种使用场景?
  8. 值传参、值返回和接收返回值时,哪些构造或拷贝可能被合并?为什么要区分初始化新对象和给已有对象赋值?

1.2 读完后完成这 3 道高频问题


1.2.1 高频问题 1:构造函数初始化列表有什么作用?哪些成员必须在其中初始化,成员缺省值和实际初始化顺序又如何影响初始化结果?


1.2.2 高频问题 2:静态成员变量和静态成员函数与普通成员有什么区别?为什么文中的对象计数和求和示例能让多个对象共同维护同一份数据?


1.2.3 高频问题 3:隐式类型转换、匿名对象与值传参、值返回时的对象构造和拷贝有什么关系?explicit 与编译器优化分别影响什么?



1.3 自检清单

  • 能说明初始化列表的作用、必须使用它的成员,以及成员缺省值的生效条件。
  • 能根据成员声明顺序分析初始化依赖,不把列表书写顺序当成执行顺序。
  • 能解释文中的隐式类型转换,以及 explicit 对转换的限制。
  • 能说明静态成员的共享方式、访问限制及其在两个示例中的作用。
  • 能区分友元函数、友元类和内部类,并说清友元关系的方向与边界。
  • 能区分匿名对象、值传参、值返回、拷贝构造与赋值,并说明文中编译器优化的差异。

2. 前言


2.1 对象从哪些入口建立

办理一张定制订单时,未填写的选项可以采用预设值,顾客明确填写的选项就按填写内容处理。有些项目必须在受理时确定,不能等订单开始制作后再补。

对应到成员初始化,声明处的缺省值是预设选项,初始化列表中明确给出的值是本次选择。引用成员、const 成员和没有默认构造函数的类类型成员,属于原文要求在初始化列表中确定的部分。各成员实际按类中的声明顺序初始化,不能从订单填写顺序推断制作顺序。

另一种进入方式是换票:窗口接受一种凭据,将它换成另一种票,但窗口也可以规定不接受自动换票。对应到类型转换,接收相应参数的构造函数让其他类型的值能够转换成类对象,explicit 用来限制文中所说的隐式转换。初始化列表关心对象内部怎样建立,转换构造关心什么输入可以成为对象。


2.2 多个对象共用一份记录

场馆入口和出口共用一块人数牌。有人进入就加一,有人离开就减一;人数牌属于整个场馆,不需要给每位访客各配一块。

在对象计数示例中,共用的人数牌对应静态成员变量 _scount,构造和析构对应计数的增减,GetACount() 对应读取记录。静态成员不属于某个具体对象,静态成员函数也没有 this 指针,因此不能直接访问非静态成员。求和示例同样利用共享记录,让多个对象的构造操作接续修改同一组计数和累计值。


2.3 访问许可与内部归属

办公室把资料放进上锁的柜子,可以单独授权一位协作者取用,也可以授权整个协作小组。授权别人取用自己的资料,并不会自动获得对方的资料权限;获授权的人再与第三方合作,也不会把权限顺带传过去。

对应到友元,柜子的限制是类的访问控制,单独授权和小组授权分别对应友元函数、友元类。它们能访问被授权类的私有和保护成员,但友元关系既不互换,也不传递,外部友元函数也不会因此变成成员函数。

内部归属解决的是另一个问题:一个部门可以把专用工作组设在部门内部,但部门的每位员工并不各自带着一支工作组。对应到内部类,它仍是独立的类,受外部类类域和访问限定符限制;外部类对象不因内部类的定义就包含内部类对象。原文把这种组织方式用于紧密关联的类,并说明内部类默认具有对外部类的友元访问权限。


2.4 临时使用与减少中间交接

临时借一把工具,完成眼前的动作就归还,不必给它安排长期存放的位置。对应到匿名对象,原文用 类型(实参) 表达临时建立、当前使用的对象,并用 Solution().Sum_Solution(10) 展示不必先为对象取名的调用方式。

减少交接是另一个问题。柜台办理材料时,若两次连续交接能直接完成,就可能省掉中间一次转手。在对象传参和返回的示例中,中间交接对应构造、拷贝构造以及返回时的临时对象;编译器可能合并其中一些步骤。原文同时保留了不同编译器的优化差异,阅读时要把源码表达的构造、拷贝、赋值关系与最终是否发生合并分开看。


3. 再探构造函数

  • 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值。构造函数初始化还有一种方式,就是初始化列表。初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
  • 每个成员变量在初始化列表中只能出现一次,语法理解上,初始化列表可以认为是每个成员变量定义初始化的地方。
  • 引用成员变量、const 成员变量、没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
  • C++11 支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。
  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表。如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显式在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++ 并没有规定。对于没有显式在初始化列表初始化的自定义类型成员,会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

初始化列表总结:

  • 无论是否显式写初始化列表,每个构造函数都有初始化列表。
  • 无论是否在初始化列表显式初始化成员变量,每个成员变量都要走初始化列表初始化。
cpp 复制代码
#include<iostream>
using namespace std;

class Time
{
public:
    Time(int hour)
        : _hour(hour)
    {
        cout << "Time()" << endl;
    }
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)
    {
        // error C2512: "Time": 没有合适的默认构造函数可用
        // error C2530: "Date::_ref": 必须初始化引用
        // error C2789: "Date::_n": 必须初始化常量限定类型的对象
    }

    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;     // 没有默认构造
    int& _ref;   // 引用
    const int _n; // const
};

int main()
{
    int i = 0;
    Date d1(i);
    d1.Print();
    return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;

class Time
{
public:
    Time(int hour)
        : _hour(hour)
    {
        cout << "Time()" << endl;
    }
private:
    int _hour;
};

class Date
{
public:
    Date()
        : _month(2)
    {
        cout << "Date()" << endl;
    }

    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    // 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的
    // 如果初始化列表没有显式初始化,默认就会用这个缺省值初始化
    int _year = 1;
    int _month = 1;
    int _day;
    Time _t = 1;
    const int _n = 1;
    int* _ptr = (int*)malloc(12);
};

int main()
{
    Date d1;
    d1.Print();
    return 0;
}

下面程序的运行结果是什么()

  • A:输出 1 1
  • B:输出 2 2
  • C:编译报错
  • D:输出 1 随机值
  • E:输出 1 2
  • F:输出 2 1
cpp 复制代码
#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();
}

4. 类型转换

  • C++ 支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  • 构造函数前面加 explicit 就不再支持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
    // 构造函数加 explicit 就不再支持隐式类型转换
    // explicit A(int a1)
    A(int a1)
        : _a1(a1)
    {}

    //explicit A(int a1, int a2)
    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:
    B(const A& a)
        : _b(a.Get())
    {}
private:
    int _b = 0;
};

int main()
{
    // 1 构造一个 A 的临时对象,再用这个临时对象拷贝构造 aa3
    // 编译器遇到连续构造 + 拷贝构造 -> 优化为直接构造
    A aa1 = 1;
    aa1.Print();
    const A& aa2 = 1;

    // C++11 之后才支持多参数转化
    A aa3 = { 2,2 };

    // aa3 隐式类型转换为 b 对象
    // 原理跟上面类似
    B b = aa3;
    const B& rb = aa3;
    return 0;
}

5. static 成员

  • 用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  • 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 this 指针。
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有 this 指针。
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  • 突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
  • 静态成员也是类的成员,受 public、protected、private 访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
cpp 复制代码
// 实现一个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;

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;

int main()
{
    cout << A::GetACount() << endl;
    A a1, a2;
    A a3(a1);
    cout << A::GetACount() << endl;
    cout << a1.GetACount() << endl;

    // 编译报错:error C2248: "A::_scount": 无法访问 private 成员(在"A"类中声明)
    //cout << A::_scount << endl;
    return 0;
}

求 1+2+3+...+n(牛客题霸,牛客网)https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13\&tqId=11200\&tPage=3\&rp=3\&ru=/ta/coding-interviews\&qru=/ta/coding-interviews/question-ranking

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;

class Solution {
public:
    int Sum_Solution(int n) {
        // 变长数组
        Sum arr[n];
        return Sum::GetRet();
    }
};

6. 友元

  • 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类。在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  • 一个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,但是 B 类不是 A 类的友元。
  • 友元类关系不能传递,如果 A 是 B 的友元,B 是 C 的友元,但是 A 不是 C 的友元。
  • 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
cpp 复制代码
#include<iostream>
using namespace std;

// 前置声明,否则 A 的友元函数声明编译器不认识 B
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;
}

int main()
{
    A aa;
    B bb;
    func(aa, bb);
    return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
    // 友元声明
    friend class B;
private:
    int _a1 = 1;
    int _a2 = 2;
};

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

    void func2(const A& aa)
    {
        cout << aa._a2 << endl;
        cout << _b2 << endl;
    }
private:
    int _b1 = 3;
    int _b2 = 4;
};

int main()
{
    A aa;
    B bb;
    bb.func1(aa);
    bb.func1(aa);
    return 0;
}

7. 内部类

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

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 A::_k = 1;

int main()
{
    cout << sizeof(A) << endl;
    A::B b;
    A aa;
    b.foo(aa);
    return 0;
}

求 1+2+3+...+n(牛客题霸,牛客网)

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;
    }
};

int Solution::_i = 1;
int Solution::_ret = 0;

8. 匿名对象

  • 用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
  • 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
cpp 复制代码
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()
{
    A aa1;
    // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
    //A aa1();

    // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
    // 但是它的生命周期只有这一行,我们可以看到下一行它就会自动调用析构函数
    // A();
    A(1);
    A aa2(2);

    // 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
    Solution().Sum_Solution(10);
    return 0;
}

9. 对象拷贝时的编译器优化

  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下,尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
  • 如何优化 C++ 标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新、更"激进"的编译器还会进行跨行跨表达式的合并优化。
  • Linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。
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=(const A& aa)" << 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 << endl;
    cout << "***********************************************" << endl;

    // 传值返回
    // 不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
    // 无优化(vs2019 debug)
    // 一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象优化为直接构造(vs2022 debug)
    f2();
    cout << endl;

    // 返回时一个表达式中,连续拷贝构造 + 拷贝构造 -> 优化一个拷贝构造(vs2019 debug)
    // 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象 aa 和拷贝的临时对象和
    // 接收返回值对象 aa2 优化为一个直接构造。(vs2022 debug)
    A aa2 = f2();
    cout << endl;

    // 一个表达式中,开始构造,中间拷贝构造 + 赋值重载 -> 无法优化(vs2019 debug)
    // 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象 aa 和拷贝临时对象合并为一个直接构造(vs2022 debug)
    aa1 = f2();
    cout << endl;
    return 0;
}

10.回答问题卡的问题


问题 1:初始化列表与构造函数体内赋值有什么区别?哪些成员必须在初始化列表中初始化?

答:

每个成员变量在初始化列表中只能出现一次,语法理解上,初始化列表可以认为是每个成员变量定义初始化的地方。成员的初始化处理发生在进入构造函数体之前,函数体里的赋值是在这之后修改成员的值。对类类型成员,只在函数体里赋值可能先默认构造、再赋值。

引用成员变量、const 成员变量、没有默认构造的类类型变量,不能指望进入函数体后再靠赋值解决初始化。文中第一份 Date 没有给这些成员声明处缺省值,所以要在初始化列表中完成;第二份示例中的 const int _n = 1 则可以使用声明处缺省值。


问题 2:成员声明处的缺省值何时生效?实际初始化顺序由声明顺序还是初始化列表顺序决定?

答:

成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。

如果这个成员在声明位置给了缺省值,并且没有在初始化列表中显式初始化,就会用这个缺省值初始化。如果显式写了初始化,就以显式提供的值为准,比如 _month(2) 会使声明处的 _month = 1 不再生效。对于没有显式初始化、也没有声明处缺省值的自定义类型成员,会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。文中 Date d1; 的 _day 没有给初值,不能据此认为它有确定的初值。

实际初始化顺序由成员在类中的声明顺序决定,跟初始化列表里的书写顺序无关。例题里先声明 _a2、后声明 _a1,所以会先执行 _a2(_a1);即使把 _a1(a) 写在初始化列表前面,也不能让它先初始化。


问题 3:哪些构造函数支持隐式类型转换?加上 explicit 后,文中的转换写法会受到什么影响?

答:

内置类型转换成类对象,需要有相关内置类型为参数的构造函数,而且这个构造函数不能加上限制该隐式转换的 explicit。不只内置类型能转换,类类型之间也可以通过相应构造函数转换,比如 B(const A&) 让 A 对象能转换成 B 对象。

构造函数前面加 explicit 就不再支持对应的隐式类型转换。文中分别给相关构造函数加上它之后,A aa1 = 1、const A& aa2 = 1、A aa3 = {2, 2},以及 B b = aa3、const B& rb = aa3 都不能再沿用对应的转换写法。显式构造仍然可以,比如 A aa1(1)、A aa3(2, 2) 和 B b(aa3)。


问题 4:为什么静态成员能用于对象计数和求和?静态成员函数没有 this 指针,会限制它访问哪些成员?

答:

因为静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。文中这些静态成员变量在类外进行初始化,可以通过 类名::静态成员 或 对象.静态成员 访问,但仍受 public、protected、private 的限制。

对象计数时,每次构造和拷贝构造都增加同一个 _scount,每次析构都减少它,所以记录的是当时仍存在的对象数。求和时,每次 Sum 构造都把当前 _i 加进同一个 _ret,再递增 _i;创建多个对象,就能接续完成累加。

静态成员函数没有 this 指针,所以不能像普通成员函数那样,直接使用某个当前对象的非静态成员;如果要访问,就需要明确指定对象,并遵守访问权限。它可以直接访问其他静态成员。


问题 5:友元函数与成员函数有什么区别?友元类关系为什么不能反向使用或传递?

答:

类内的 friend 语句是友元声明,这里的外部友元函数不是类的成员函数,但可以访问被授权类的私有和保护成员。成员函数属于类;文中的外部友元函数 func 在类外定义和调用,通过参数拿到 A、B 对象,再访问它们的成员。

友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,但是 B 类不是 A 类的友元。友元类关系不能传递,如果 A 是 B 的友元,B 是 C 的友元,但是 A 不是 C 的友元。每一份访问许可都需要由相应的类单独授予。


问题 6:内部类的独立性、访问权限与外部类对象的组成有什么关系?求和示例为何把 Sum 放进 Solution 内部?

答:

内部类是一个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象不会仅仅因为这个类型定义,就包含内部类对象。

内部类默认是外部类的友元类。内部类具有访问外部类私有和保护成员的权限。文中的 B::foo 可以直接使用静态成员 _k,访问非静态成员 _h 时,则通过传入的外部类对象 a。

因为 Sum 和 Solution 紧密相连,Sum 主要是给 Solution 使用,所以把它放进 Solution 内部。这个示例中,Sum 的构造操作负责更新 Solution 的静态计数和累计值。


问题 7:匿名对象与有名对象在写法和生命周期上有什么差别?Solution().Sum_Solution(10) 展示了哪种使用场景?

答:

匿名对象用 类型(实参) 创建,不需要对象名;有名对象用 类型 对象名(实参) 定义。匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。

对文中的 A(1); 和 Solution().Sum_Solution(10); 这种独立语句,临时对象在该语句结束时销毁;aa1、aa2 这样的局部有名对象则保留到所在作用域结束。

Solution().Sum_Solution(10) 展示的是匿名对象只需要使用一次的场景:临时创建一个 Solution,直接调用它的成员函数,不必先为对象取名。


问题 8:值传参、值返回和接收返回值时,哪些构造或拷贝可能被合并?为什么要区分初始化新对象和给已有对象赋值?

答:

值传参和返回值传递中的一些连续构造、拷贝构造可能被合并;但初始化新对象和给已有对象赋值,是两种不同的操作。

文中的 f1(aa1) 是用已有对象拷贝构造形参;f1(1) 和 f1(A(2)) 涉及从实参构造对象、再交给值形参的过程,这些连续步骤可能合并。f2() 返回局部对象时,局部对象与返回结果之间的拷贝也可能被省略;用 A aa2 = f2() 接收返回值时,新对象的初始化还可能与前面的步骤合并。

在文中所举的编译器示例里,同一表达式中的连续构造、拷贝可以合并,有些示例还展示了跨行、跨表达式的合并。具体调用次数不能只看这些概括,还要限定到原文所举的编译环境。

A aa2 = f2() 是初始化新对象,虽然写了 =,也不是给已有对象赋值。aa1 = f2() 中的 aa1 已经存在,最后走的是赋值运算符 operator=;右侧返回过程可以减少中间构造或拷贝,但这不会把赋值变成拷贝构造。


高频问题 1:构造函数初始化列表有什么作用?哪些成员必须在其中初始化,成员缺省值和实际初始化顺序又如何影响初始化结果?

答:

构造函数初始化列表负责在进入函数体之前确定成员的初始状态。函数体里的赋值是在这之后再修改成员,不能替代那些必须先完成的初始化。

文中第一份 Date 的引用成员、const 成员和没有默认构造函数的类类型成员,没有可用的声明处缺省值,所以要在初始化列表中分别处理。声明处已经给出合适缺省值时,未显式列出的成员可以使用它;如果列表里显式给值,就使用显式值。例如 _month(2) 覆盖声明处的 _month = 1,而没有显式列出的 _year 使用声明处的 1。

实际初始化顺序按成员在类中的声明顺序决定,与列表的书写顺序无关。因此,成员之间有初始化依赖时,要先看谁先声明,不能靠调整列表顺序让后声明的成员提前初始化。


高频问题 2:静态成员变量和静态成员函数与普通成员有什么区别?为什么文中的对象计数和求和示例能让多个对象共同维护同一份数据?

答:

普通非静态成员变量在各个对象中各有一份,静态成员变量则由所有对象共享,文中的静态数据不放在每个对象里。普通成员函数通过 this 关联当前对象,静态成员函数没有 this,因此不能直接使用当前对象的非静态成员。

对象计数示例只有一份 _scount。普通构造和拷贝构造都递增它,析构递减它,GetACount() 读取这份共享记录,所以得到的是当时仍存在的对象数。

求和示例也只有一份 _i 和 _ret,初始分别为 1 和 0。每创建一个 Sum 对象,它的构造函数就执行 _ret += _i,再执行 ++_i;创建 n 个对象时,各次构造接着上一轮的状态继续累加,就能从初始状态完成 1 + 2 + ... + n。

静态成员仍受访问权限限制。文中的私有计数不能由类外随意读取,但可以通过公开的静态成员函数访问,也可以使用 类名::函数名 的写法调用。


高频问题 3:隐式类型转换、匿名对象与值传参、值返回时的对象构造和拷贝有什么关系?explicit 与编译器优化分别影响什么?

答:

隐式类型转换可以通过相应构造函数,把其他类型的实参变成类对象;匿名对象则是在需要时临时创建对象,不为它取名。它们都可能出现在值传参或值返回的过程中,但关注的事情不同。

例如 f1(1) 需要把整数转换成 A 类型的值形参,f1(A(2)) 则显式构造 A 再用于值传参;f1(aa1) 是用已有对象初始化值形参。f2() 按值返回局部对象,再用 A aa2 = f2() 接收时,也涉及返回结果和新对象的初始化。文中用这些写法展示了连续构造、拷贝可能被合并的现象。

explicit 影响的是能否使用对应的隐式转换写法,例如给 A(int) 加上它后,f1(1) 不能再这样自动转换,但 f1(A(2)) 仍明确构造了实参。编译器优化影响的是合法写法中哪些中间构造、拷贝步骤可以合并;它不负责让被 explicit 限制的隐式转换重新合法。

接收返回值时还要区分:A aa2 = f2() 是初始化新对象;aa1 = f2() 是给已有对象赋值,最后的赋值操作不能被说成拷贝构造。

相关推荐
万联WANFLOW34 分钟前
异地厂区视频监控远程运维:从组网到安全的实施要点
运维·网络·安全·企业组网·sdwan
旖旎夜光1 小时前
【C++ 基础入门】类和对象(上):封装、对象模型与 this 指针
开发语言·c++·笔记·学习
SL_staff1 小时前
打破风控迭代死循环:用决策编排重构业务与技术协作链路
java·产品·全栈
衍玊1 小时前
深入剖析 TCP/IP 网络通信原理与内核实现
服务器·网络·tcp/ip
Mikko71 小时前
Redis 连接数满了服务端有错误信息吗?连接数不增加却命令超时?6.2 实测连接池、超时与大 key 怎么判定
java·spring boot·redis·后端
.Bryce.2 小时前
从零到一开发技术名词大全:从需求分析到 AI Agent 工程
java·网络·人工智能·agent·需求分析·agent评测
小超人诞生日记2 小时前
C++ 智能指针的使用及其原理详解
c++·算法
SL_staff2 小时前
JVS-APS的7个刚性约束设计:从无限产能幻觉到可执行排产的工程实践
java·spring cloud·全栈
akinggw3 小时前
libagentcore - 一个基于c++的AI多agent系统升级到2.0版本
开发语言·jvm·c++
Black蜡笔小新3 小时前
上云网关/端口穿透/设备上云EasyNTS:无需公网IP、也不改防火墙,设备怎么从内网跑到外网去
网络·网络协议·tcp/ip