Skip to content

ch12 define a class

主要讲述类的相关问题。

构造函数

以下先列出几乎所有的构造函数,解释其含义,与是否自动生成:

构造函数类型含义是否默认生成
默认构造函数无参数的构造函数默认生成
带参数的构造函数多种形式的带参数的构造函数不默认生成
拷贝构造函数通过对一个对象进行拷贝进行构造新的对象默认生成
移动构造函数转移一个对象的资源、控制权、或者说是转移一个即将消失的对象默认生成
委托构造函数在一个构造函数中调用别的构造函数来生成对象不默认生成

用户在定义构造函数之后(甚至不一定是默认构造函数),编译器就不会再生成默认构造函数,如果想使用编译器自动生成的构造函数,需要在类中函数的声明后添加 =default,来请求编译器自动生成,如果在默认构造函数中没什么特殊的功能需要实现,建议使用编译器自动生成的构造函数。

构造函数的默认参数

首先第一点是 cpp 建议所有的默认参数都设置在声明中,而不是在定义中,有如下理由:

  1. 提高可读性。将默认参数在声明的时候设置,能够在看到声明的时候就直接知道默认参数,增加可读性。
  2. 解决链接时候的问题,将默认参数设置在声明中能解决链接时候的问题。加入将默认参数设置在定义中,在用过声明使用的时候并看不到默认参数,可能会遇到编译甚至链接问题。

而对于类的构造函数的默认参数,需要注意的是,不能将其所有的参数都设置默认参数,如果将所有的参数都设置默认参数,在调用的时候就能够不传递任何参数,这就和默认构造函数产生了冲突,因此构造函数不能将参数全部指定为默认参数。

通过 explicit 禁用隐式转换

可以通过 explicit 禁止构造函数或者运算符的隐式转换从而避免错误的产生。

cpp
class MyClass {
public:
    explicit MyClass(int value) {}
};

MyClass obj = 10; // 错误:不能隐式转换 int 到 MyClass
MyClass obj(10);  // 正确:显式调用构造函数

对于单参数的构造函数(包括多个参数,但是除了第一个参数其他都是默认值的参数)应当开启 explicit,除非认为隐式转换是可以接受的默认行为。

this 指针

this 指针,每个类的对象都有 this 指针指向当前对象,静态变量没有。

const 对象和 const 成员函数

创建对象的时候可以将对象指定为 const,对对象施加的 const 等于对对象中全部的成员施加 const。类之中可能会含有引用变量和指针变量,对于指针变量,const 是加在指针上,说明指针所指向的地址不可变但是所指向地址上的值是可变的。引用同理,因为引用本身就是一次赋值不可改变的天然 const 属性,但是引用代表的别名变量在这个过程中数值是可以更改的。

这时候就会有一个问题,如果类中的方法会改变对象的值,那这个方法还如何给 const 对象调用。显然编译器不允许这种行为,否则就违反了 const 的要求。因此 cpp 引入了 const 成员函数,对成员函数标记的 const 能够给 const 对象调用。因此现在普通对象能够调用任何的成员函数,但是 const 对象只能调用带有 const 标记的函数。

实际上,在调用方法的时候,普通的对象在方法中的指针是普通的 this 指针,在具有 const 标记的方法中,这个 this 指针也是带 const 的,这样能够更好的理解。

const 成员函数中,也不能够返回非 const 的引用和指针,总之一旦加上 const,所有可改变的路都被堵死。

常量的强制转换和 mutable 关键字

有非常少的场景下存在对常量进行强制转换的需求,将 const 变成非 const,这个操作主要使用 const_cast<> 完成,使用的场景如下:

  1. 修改常量对象,一定是很确定的情况下才需要这么做。
  2. 和旧式的 C 代码交互,这还是很重要的。

同时也可以设置代码中的成员变量为 mutable,这样这个成员变量就不受创建的对象是否是 const 的影响。

cpp
#include <iostream>
#include <format>

class A{
    public:
        mutable int t;
        A() = default;
        A(int t):t(t){

        };
        ~A() = default;
};

int main(){
    const A a{10};
    std::cout << std::format("t is {}", a.t) << std::endl;
    a.t++;
    std::cout << std::format("now t is {}", a.t) << std::endl;

    return 0; 
}

这里会分别输出 10 和 11。

友元

友元关系是单向关系, A 为 B 的友元并不代表 B 是 A 的友元。

友元声明并不代表函数声明和类声明,友元声明仅仅表示友元关系而已。在将一个函数或者一类类声明为友元之前,这个函数和类都是要先前向声明的。

类对象的大小与静态成员、静态方法

一个类对象的大小等同于类中非静态变量的大小,在必要时可能考虑对于体系结构的对齐。类中的静态变量总是在类中声明,可选择的在类外定义,但是在类定义的时候不能使用 static 关键字,否则会报错。对于静态成员变量的访问能通过类名直接访问也能够通过对象名访问,但是建议使用类名直接访问,这样语义更加清晰。

对于类中的静态函数,其不能指定为 const,静态函数不和任何对象关联,自然没有 this 指针,自然也没有 const 函数,显然的静态函数也不能访问类中非静态的变量。静态函数对传入的当前类的对象能够访问全部的属性,这是其特点,当然没什么这么做的必要,更好的方式是通过你内部类完成这个功能。