const

const成员函数

要点:

  • 成员函数都有一个隐含参数this
  • 对于非const成员函数,this的类型是A* const: 指针是const(不能改指针本身),但指向的对象可被修改。
  • 对于const成员函数(在函数名后写了const),this的类型是const A* const: 指针不可变且指向const A,表示不能修改对象的非mutable成员。
  • 成员函数后加const表示“承诺不修改对象的可观察状态”。对应的隐含this类型是const A* const(指针不可改且指向const对象)。
    有时在查询函数中希望做惰性计算/缓存(例如计算 Fibonacci 并缓存结果),这属于“内部实现细节”,从外部观察对象状态并不改变。
    为了在const函数内仍能修改这些内部缓存,使用mutable修饰成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A {
int x;
public:
void f() { x = 1; } // 等价为void f(A* const this); 如果函数体内写this = nullptr; 编译错误,this是const指针
void show() const {} // 等价为void show(const A* const this); 如果函数体内写x = 2; 编译错误,this指向const A
mutable int m;
void show2() const { m = 3; } // 合法,mutable成员可在const函数中修改
};

struct A {
int &r; // 非 const 引用
const int &cr; // const 引用
mutable int m; // 可在 const 函数中修改

void f() const {
r++; // 合法:修改r所指的对象(this未改变r本身)
cr++; // 错误:cr是const int&,不能修改被指对象
m++; // 合法:mutable成员可改
}
};

constant expression

常量表达式是在编译期即可计算出结果的表达式:

  • 把计算前移到编译期程序运行得更快且错误也能够更早被发现。

  • constexpr:允许在编译期求值的函数或变量(也能在运行期使用)。如果上下文要求常量表达式,编译器会在编译期求值。

  • consteval(C++20):强制立即函数,只能出现在函数声明上,调用时必须在编译期求值;如果调用点不是常量表达式会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // constexpr:可编译期求值,也可运行期调用
    constexpr int sq(int x) { return x * x; }
    int a[sq(3)]; // 编译期求值 -> a[9]
    int n = 4;
    int v = sq(n); // 运行期求值(n 只有运行期才知道)

    // consteval (C++20):必须在编译期求值
    consteval int two() { return 2; }
    int b = two(); // 必须是编译期求值
  • 适用场景:数组大小、非类型模板实参、switchcase标签、查表/位运算等需要编译期常量的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    enum Flags { GOOD=0, FAIL=1, BAD=2, EOF=3 };
    int operator|(Flags a, Flags b); // 在这个表达式前面加上constexpr就可以通过

    // 或者用 constexpr 辅助函数:
    // constexpr int bad_c() { return int(BAD); }
    // constexpr int eof_c() { return int(EOF_); }
    // constexpr int be_c() { return bad_c() | eof_c(); }
    // case be_c(): // 合法

    void f(Flags x) {
    switch(x) {
    case BAD: break;
    case EOF: break;
    case BAD | EOF: // 可能报错:不是编译期整常量
    break;
    }
    }

static

静态成员是属于类本身的成员(类级别),不是某个对象的成员。
同一类的所有对象共享同一份静态数据(只有一份拷贝)。
当需要不同对象共享一个变量时,不用把它做全局变量(否则缺乏封装并产生命名污染)。

static成员变量

1
2
3
4
5
6
class A {
int x, y;
static int shared;
};
int A::shared = 0;
A a,b;
  • 同一类的所有对象共享同一份静态数据(只有一份拷贝)。
  • 声明与定义:
    • 在类内声明:static int shared
    • 在类外定义并初始化(非 inline 情况):int A::shared = 0
    • C++17起可用inline初始化:inline static int shared = 0(不需类外定义)。

static成员函数

1
2
3
4
5
6
7
class A {
static int shared;
int x;
public:
static void f() {} // 只能访问shared
void q() {} // x和shared都可以访问
}
  • static函数没有隐含的this指针,因此不能访问非静态成员(实例成员x)或this
  • 静态函数可以访问和修改静态成员变量(类级别的共享数据)。
  • 遵循类访问控制

static成员的使用

因为C++是支持观点”类也是对象”的,所以可以通过A::f()obj.f()(推荐用第一种方式访问)。这也是拷贝构造函数和移动构造函数的基础。

示例(单例模式的实现):

1
2
3
4
5
6
7
8
9
10
11
12
class singleton {
singleton() = default;
public:
singleton(const singleton&) = delete;
singleton& operator=(const singleton&) = delete;
static singleton& instance() {
static singleton inst;
return inst;
}
};

singleton& s = singleton::instance();

友元

因为类的访问控制规定了类的外部不能访问该类的private成员,只能通过该类提供的public方法当作接口来访问私有成员,但是这会降低对私有成员的访问效率并且缺乏灵活性。

为了解决上述的问题提出了友元:

  • 分类:
    • 友元函数
    • 友元类
    • 友元类成员函数
  • 作用:
    • 允许外部函数/类访问本类的private/protected成员(绕过普通访问控制)。
    • 提高设计灵活性,常用于实现高效的操作/接口。
    • 在“数据保护”和“访问效率”之间提供一种折中方案:既能保持封装界面,又能避免过多的访问器函数开销。
1
2
3
4
5
6
7
8
9
10
11
void func();
class B;
class C {
void f();
};

class A {
friend void func(); // 友元函数
friend class B; // 友元类
friend void C::f(); // 友元成员函数
}
  • 友元不具传递性:A把f声明为友元并不自动让f成为B的友元;要访问B的私有成员,必须也在B中声明f为友元。
  • 友元不继承:派生类不会继承基类给某函数的友元资格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前向声明
class Matrix;
class Vector;
void multiply(Matrix& m, Vector& v, Vector& r);

class Matrix {
int a;
friend void multiply(Matrix&, Vector&, Vector&);
};

class Vector {
int b;
friend void multiply(Matrix&, Vector&, Vector&);
};

void multiply(Matrix& m, Vector& v, Vector& r) {
// 可以访问 m.a 和 v.b 等私有成员
}

前向声明尽管在某些条件下是可以省略的,但可能会出现命名污染等问题,所以在使用友元时一定要保证友元在此之前已被恰当声明。