• 【C++】《C++ Primer》第七章:类(知识点总结)


    目录

    7.1 定义抽象数据类型

    const成员函数和this指针

    返回this对象

    构造函数的性质

    合成的默认构造函数

    默认构造函数和default

    初始化列表

    7.2 访问控制与封装

    友元

    友元的声明

    声明友元不等于声明其函数(友元的作用域)

    7.3 类的其它特性装

    内联函数的声明

    返回*this的成员函数

    基于const重载函数

    编程习惯!

    7.4 类的作用域

    定义在类外部的成员

    编程习惯!

    7.5 构造函数再探

    构造函数初始化的顺序

    关于默认构造函数

    隐式的类类型转换

    阻止:隐式的类类型转换

    7.6 类的静态成员

    静态成员

    静态成员的初始化


    7.1 定义抽象数据类型

    const成员函数和this指针

    this指针,在默认情况下,是类类型非常量版本的常量指针,例如下面的Person类的this指针默认是Person *const this

    意思就是:默认情况下,不能修改this指针指向的对象是谁,但可以通过this指针修改指向对象的值。

    然而,如果有一个成员变量是const类型,例如下面的const int m_a,而成员函数func要用到这个成员变量,但又不希望修改它,因此需要在函数后面加一个const

    1. class Person
    2. {
    3. public:
    4. void func(int a) const
    5. {
    6. this->m_a = a; // 错误,因为上面声明了const,因此this指向的成员不可修改
    7. cout << m_a;
    8. }
    9. const int m_a = 10;
    10. };
    11. void test()
    12. {
    13. Person p;
    14. p.func(20);
    15. }

    返回this对象

    1. class Person
    2. {
    3. public:
    4. Person& add(Person p) // 返回Person引用,也对应了 return *this;
    5. {
    6. this->Age += p.Age;
    7. return *this;
    8. }
    9. int Age = 10;
    10. };
    11. void test()
    12. {
    13. Person p1, p2;
    14. p1.Age = 10;
    15. p2.add(p1);
    16. cout << p2.Age;
    17. }

    return *this返回的就是当前这个对象,例如p2.add(p1),那么这个this指的是p2这个对象,因此第6-7行等价于:p2.Age += p1.Age; return &p2;

    或者说,add()成员函数的返回值是调用add()的对象的引用,第16行是p2调用add,因此add()返回的那个*this就是p2的引用。通过输出地址,可以发现,this的地址和p2的地址是同一个。

    构造函数的性质

    • 构造函数不能被声明成const型。当声明一个const对象时,需要通过构造函数向其写值(初始化过程),只有在初始化完成之后,这个对象才具有const属性。
    • 构造函数没有返回类型。

    合成的默认构造函数

    概念:如果我们没有显式地写一个构造函数,那么编译器会帮我们自动创建一个隐式的构造函数,就叫做“合成的默认构造函数”。

    默认构造函数和default

    显式补充默认构造函数,不然的话,有了其它构造函数后,就会丢失默认构造函数。

    1. class Person
    2. {
    3. public:
    4. Person() = default; // 显式定义一个默认构造函数
    5. // Person() {}; // 和上面等价,但只能二选一
    6. Person(int age, int id, string name) : Age(age), Id(id), Name(name) {}; // 自己定义其它构造函数
    7. int Age = 10;
    8. int Id = 20;
    9. string Name = "wind";
    10. };
    11. void test()
    12. {
    13. Person p1(20, 101, "wind");
    14. Person p2; // 调用第4行的默认构造函数
    15. }

    如果没有第4行,则第15行就出错,因为我们已经在第5行定义了一个构造函数,所以编译器就不会自动创建一个合成的默认构造函数!因此,需要自己加上一个默认构造。

    初始化列表

    格式如下:

    1. class Person
    2. {
    3. public:
    4. // 初始化列表格式:
    5. Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};
    6. int Age = 10;
    7. int Id = 20;
    8. string Name = "wind";
    9. };

    7.2 访问控制与封装

    友元

    作用:其它类或者函数能够访问类内部私有成员。

    注意:

    • 只能定义在类的内部
    • 可以定义在内部的任何位置,不论是public还是private都行,不过建议在类定义开始或结束前集中声明。
    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};
    6. friend void test(int a); // friend + 函数声明(要和函数声明一模一样)
    7. private:
    8. int Age = 10;
    9. int Id = 20;
    10. string Name = "wind";
    11. };
    12. void test(int a)
    13. {
    14. Person p1(20, 101, "wind");
    15. Person p2;
    16. cout << p2.Age; // 如果test函数不是友元,那么这句就报错,因为无法访问私有成员Age
    17. }

    友元的声明

    因为友元只是制定了访问权限,并不是真正的函数声明,如果其它函数需要调用这个友元函数,那么必须在专门针对这个函数做一个声明。通常情况,把针对友元的声明和类本身放在同一个头文件中

    要注意几点:

    • 如果把整个类A作为另一个类B的友元,那么类A里的所有成员都可以访问B的所有成员。
    • 把类A的成员函数作为类B的友元,那么在声明的时候,必须指出该成员函数属于那个类,例如:
    1. class Student{public:void func();};
    2. class Person
    3. {
    4. friend void Student::func(); // 必须有Student::
    5. }
    • 函数重载后,实际上就是2个不同的函数了,因此需要把这2个函数都声明为友元。(这也是声明友元时,必须要把形参写进去的原因)
    1. class Student
    2. {
    3. public:
    4. void func();
    5. void func(int a);
    6. };
    7. class Person
    8. {
    9. friend void Student::func();
    10. friend void Student::func(int a); // 如果不写这个,那么这个版本的func就无法访问Student的私有成员
    11. }

    声明友元不等于声明其函数(友元的作用域)

    7.3 类的其它特性装

    内联函数的声明

    • 在类内部定义的成员函数,会被自动声明为 inline(隐式内联)
    • 为了更容易理解,建议只在类外部定义的地方说明函数是inline

    即使const,也能修改变量值:mutable

    关键字:mutable

    作用:把一个变量变成“可变成员”

    1. class Person
    2. {
    3. public:
    4. void func(int a) const
    5. {
    6. this->m_a = a; // 正确,即使函数是const, 但m_a是mutable的
    7. this->m_b = a; // 错误,因为m_b不是mutable, 因此不可被修改
    8. cout << m_a;
    9. }
    10. mutable int m_a = 10;
    11. int m_b = 10;
    12. };

    返回*this的成员函数

    一大作用:可以把一系列操作串起来。

    1. 伪代码:
    2. Person& add(int a)
    3. {
    4. ...
    5. return *this;
    6. }
    7. Person& sub(int a)
    8. {
    9. ...
    10. return *this;
    11. }
    12. Person p;
    13. p.add(1).sub(2); // 串起来操作,意思是先加1,再减2

    注意:针对返回的函数定义为const,那么返回的*this也会变成const对象。

    基于const重载函数

    直接看例子:

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. Person(int num) : Num(num) {};
    6. // 非const重载的display()
    7. Person& display()
    8. {
    9. cout << "非const: " << this->Num << endl;
    10. return *this; // 返回非const对象
    11. }
    12. // const重载的display()
    13. const Person& display() const
    14. {
    15. cout << "const: " << this->Num << endl;
    16. return *this; // 返回const对象
    17. }
    18. Person& add(int a)
    19. {
    20. this->Num += a;
    21. return *this;
    22. }
    23. int Num;
    24. };
    25. void test()
    26. {
    27. Person p1(10); // 非const对象
    28. const Person p2(20); // const对象
    29. p1.display(); // 调用非const的display()
    30. p1.add(10).display(); // 调用非const的display()
    31. p2.display(); // 调用const的display()
    32. }

    如果没有写非const重载的display(),那么最后p1就会调用const重载的display(),对应《C++ Primer》第248页最上面:

    如果让add变成constNum变成mutable,那么p1.add(10).display()就会调用constdisplay(),因为此时p1.add(10)返回的是const对象,自然会首选constdisplay()

    编程习惯!

    对于公用代码使用私有功能函数,就像上面为输出结果而专门写的小函数display(),原因如下:

    • 后期只需要维护这个小函数即可,就不用到处找相关代码了
    • 调试版本中可以在函数里假如调试信息,发行版就便于去掉。
    • 因为在类内定义的这些小函数,因此会自动变为inline,因此后面调用它的时候就不会带来额外的运行开销。

    7.4 类的作用域

    定义在类外部的成员

    《C++ Primer》第243页讲到定义一个类型成员:

    也就意味着,这个pos的作用域就是类内部,如果在外面访问,需要加上Person::表面其作用域;同理,类内声明的函数也是如此:

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. Person(int num) : Num(num) {};
    6. using type1 = int; // 定义类型
    7. type1 func(type1 a); // 声明函数
    8. };
    9. Person::type1 Person::func(Person::type1 a)
    10. {
    11. cout << a;
    12. }

    重点在第11行:

    如果type1前不加Person::,那么会报错"未定义标识符"。

    如果func前不加Person::,那么这里定义的func就不是第8行声明的那个func,而是一个新的函数,返回类型是type1,也就是int,等价于:int func(int a){cout << a;},和Person完全无关。

    如果保持11行的代码,那么调用int a = p1.func(1);的时候,就会报错,因为func重定义了。

    编程习惯!

    即,把类似using xxx = xxx;typedef xxx yyy;放在类的开始处。

    7.5 构造函数再探

    构造函数初始化的顺序

    成员的初始化顺序与它们在类定义中的出现顺序一致。

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. int Age;
    6. int Id;
    7. Person(int val) : Id(val), Age(Id) {}; // 错误
    8. Person(int val) : Age(val), Id(Age) {}; // 正确
    9. };
    10. void test()
    11. {
    12. Person p1(10);
    13. cout << p1.Age << endl << p1.Id << endl;
    14. }

    Age先定义,Id后定义,因此:

    8行是正确的:先用初始化Age--使用val,再初始化Id--使用Age

    7行是错误的:先用初始化Age--使用Id,而此时Id还没有被初始化,因此输出p1.Id是个乱码。

    注:应该尽量按顺序初始化,并且不要用某些成员去初始化其它成员(例如用Id去初始化Age)。

    关于默认构造函数

    如果类A没有默认构造函数,类B有一个类A的成员,那么想要定义类B的默认构造函数,就必须显式调用A的带参构造函数初始化这个类A的成员:

    1. class Person
    2. {
    3. public:
    4. //Person() = default;
    5. Person(int a) {}; // 只有一个带参构造函数
    6. int Val = 10;
    7. };
    8. class C
    9. {
    10. public:
    11. Person p;
    12. C(int i = 0) : p(i) {}; // 写C的默认构造的时候,必须显式调用A的带参构造函数初始化成员p
    13. };
    14. void test()
    15. {
    16. C c;
    17. cout << c.p.Val;
    18. }

    如果第13行改为:C(int i),那会第18行报错,提示不存在默认构造函数。也就是说,Person没有默认构造,因此无法用(int i) : p(i)的方式去初始化p,只能用值初始化的方式。

    如果第4行存在,那么Person就有默认构造函数了,从而可以省略第13行。

    隐式的类类型转换

    大前提:只有“具有1个实参”的构造函数能够隐式转换,没有实参、有多个实参的构造函数都不行。

    例如,下面第21-22行,就是通过实参调用构造函数,然后就会把这个int型转换为Person型。实际上是通过创建一个临时Person对象来过渡的:

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. Person(int a) {};
    6. Person& add(const Person& p)
    7. {
    8. this->Val += p.Val;
    9. return *this;
    10. }
    11. int Val = 10;
    12. };
    13. void test()
    14. {
    15. Person p1(10), p2;
    16. int a = 10;
    17. p2.add(a);
    18. p2.add(int(10));
    19. cout << p2.Val;
    20. }
    21. 20-21行等价于:
    22. int a = 10;
    23. Person temp(a); --> 临时对象
    24. p2.add(temp);

    阻止:隐式的类类型转换

    关键词:explicit

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. explicit Person(int a) {};
    6. ...
    7. };
    8. void test()
    9. {
    10. Person p1(10), p2;
    11. int a = 10;
    12. p2.add(a); // 错误!因为第5行声明了explicit,不再允许!
    13. cout << p2.Val;
    14. }

    注1:explicit只允许出现在类内的构造函数声明处,类外不行!

    注2:explicit只是防止隐式转换,如果要强制转换,explicit就没有用了:

    1. class Person
    2. {
    3. public:
    4. Person() = default;
    5. explicit Person(int a) {};
    6. ...
    7. };
    8. void test()
    9. {
    10. Person p1(10), p2;
    11. int a = 10;
    12. p2.add(static_cast(a)); --> 对其强制转换
    13. cout << p2.Val;
    14. }

    7.6 类的静态成员

    静态成员

    关键字:static

    主要为了调用方便,不需要生成对象就能调用:

    1. class X
    2. {
    3. public:
    4. void MethodA();
    5. static void MethodB();
    6. }

    此时MethodB可以直接调用,X::MethodB();

    MethodA必须先生成类对象才能调用,X x; x.MethodA();

    注意:

    • 存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
    • 静态成员函数不与任何对象绑定在一起,因此不包含this指针。
    • 不能被声明为const
    • 因为是static,所以生命周期是持续到程序结束。
    • static关键字只能在类内部,如果在类外部定义静态成员,那么不能重复static关键字:
    1. class Person
    2. {
    3. public:
    4. static Person& add(const Person& p)
    5. {
    6. this->Val += p.Val; // 错误,因为声明为static后,不包含this指针
    7. return *this;
    8. }
    9. static void func();
    10. int Val = 10;
    11. };
    12. static void Person::func() {} // 错误,不能在类外重复关键字static
    13. void Person::func() {} // 正确

    静态成员的初始化

    • 通常情况下,不应该在类内对其进行初始化;如果要初始化,必须用constconstexpr
    • 除了静态常量成员,其它静态成员不能在类内初始化:
    1. class Person
    2. {
    3. public:
    4. static const int a = 10; // 正确,a是个静态常量成员
    5. static vector<int>v(a); // 错误
    6. static vector<int>v; // 正确,必须在类外初始化
    7. };
    • 静态成员可以作为默认实参,普通成员不行。
    • 静态成员可以是不完全类型(只声明,但未定义),例如:
    1. class Person
    2. {
    3. public:
    4. static person p1; // 正确,静态数据成员可以是非完全类型
    5. Person *p2; // 正确,指针类型也可以是非完全类型
    6. Person p3; // 错误,非静态数据成员必须是完全类型
    7. }

    小结:只要想到“不和任何对象绑定在一起”,再能理解这些了。

  • 相关阅读:
    ChatGPT和文心一言分析茅台与瑞幸的联名盛宴:酱香拿铁背后的商业布局
    Java -- this关键字
    K8S集群master节点打污点:可让master节点参与pod调度
    测试用例的8大设计原则
    并查集(Union-Find)
    XXXX项目管理目标(某项目实施后基于软件工程的总结)
    【TS】函数重载--可选参数--默认参数
    JavaScript奇淫技巧:清理无效的垃圾代码
    uniapp+vue3+ts+uview-plus搭建项目步骤
    PHP(1)搭建服务器
  • 原文地址:https://blog.csdn.net/Wind_2028/article/details/127685311