c++常量

const

修饰变量

const关键字可以用来防止对象发生变化。一个const对象必须有以下特征:

  • 必须已经初始化
  • 不能被修改
  • 是线程安全的
  • 只能调用const成员函数

1
const int ci = 1;

指针

常量指针

指向常量的指针,简称常量指针。

1
2
const int ci = 1;
const int* pci = &ci;

指针常量

指针本身是一个常量。

1
2
int i = 1;
int const* cpi = &i;

引用

常量左值引用

1
2
3
4
5
6
7
int i = 1;
const int ci = 1;

const int& cir = i;
const int& cir2 = ci;
int& cr = 1; // error
const int& cir3 = 1;

常量右值引用

1
2
3
int i = 1; 
const int&& cri = i; // error
const int&& cri2 = 1;

修饰函数

const修饰的成员函数不能修改当前对象的属性

1
2
3
4
5
6
class A {
int val = 1;
void canNotModify() const {
val = 13; // error
}
};

物理常量和逻辑常量

  • 物理常量,也叫比特位常量,当前对象的每一个比特都不能被修改,这也是c++对于const的实现。
  • 逻辑常量,有些时候,一个const函数依旧希望能修改某些比特位,但是其从逻辑上讲依旧是const的。

mutable

mutable用来打破物理常量的限制,实现逻辑常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ThreadSafeCounter {
mutable std::mutex m;
int counter = 0;
public:
int get() const {
std::lock_guard<std::mutex> lk(m);
return counter;
}
void inc() {
std::lock_guard<std::mutex> lk(m);
++counter;
}
};

int main() {

std::vector<std::thread> vec;
ThreadSafeCounter counter;
for (int i = 0; i < 20; ++i){
vec.emplace_back([&counter] {
counter.inc();
std::cout << "counter: " << counter.get() << '\n';
counter.inc();
});
}

}

const_cast

const_cast可以添加或删除const/volatile修饰符到一个变量上。

1
2
3
4
5
6
7
8
9
10
11
12
13
void func(int* ){ }
void funcConst(const int*){ }

int main() {
const int myInt{1988};
func(&myInt); // error
int* myIntPtr = const_cast<int*>(&myInt);
func(myIntPtr);

const int* myConstIntPtr = const_cast<const int*>(myIntPtr);
funcConst(myConstIntPtr);
funcConst(myIntPtr);
}
1
2
3
4
5
6
7
8
9
10
#include <iostream>
int main() {
const int ca = 10;
const int* pca = &ca;
int* pa2 = const_cast<int*>(pca);
*pa2 = 12; // undefined behavior
std::cout << *pa2 << " " << pa2 << '\n';
std::cout << *pca << " " << pca << '\n';
std::cout << ca << " " << &ca << '\n';
}

通过const_cast删除常量修饰从而修改常量的行为是未定义的!!

constexpr

修饰变量

  • constexpr是隐式const

    1
    2
    3
    4
    5
    6
    int main() { 
    const int a = 1;
    constexpr int b = 2;
    a = 3; // error
    b = 3; // error
    }
  • constexpr变量只能接收常量表达式(在编译时可以确定值),与const不同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main() { 
    int c = 1;
    const int a = c;
    constexpr int b = c; // error
    constexpr int b2 = a; // error

    const int a2 = 2;
    constexpr int b3 = a2;
    }

修饰函数

  • constexpr修饰的函数,可以是编译时,也可能是运行时
  • constexpr函数中不能使用static、thread_local
  • 所有依赖必须都是编译时,函数才会是编译时(允许常量表达式初始化的变量)
  • 被constexpr接收时,函数必须是编译时
  • 如果是编译时运行,是纯函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    constexpr int add(int a, int b) {
    // static int c = 10; // error
    return a + b;
    }

    int main() {
    // 不同的编译器实现不同,可能是编译时也可能是运行时
    int i = add(1, 3);
    constexpr int b = add(1, 2);

    int v = 3;
    int c = add(v, 3);
    //constexpr int a = add(v, 3); // error
    constexpr int e = add(b, 2);
    return 0;
    }

类函数

  • 至少有一个constexpr修饰的构造函数
  • 类中可以定义constexpr函数和非constexpr函数
  • constexpr修饰的类对象只能调用constexpr修饰的成员函数
    1
    2
    3
    4
    5
    class MyDouble {
    double myVal;
    constexpr MyDouble(double v) : myVal(v) {}
    constexpr double getVal() {return myVal;}
    };

C++20

从C++20开始,支持在constexpr函数中使用stl(编译时分配的内存必须在编译时释放)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <algorithm>
#include <iostream>
#include <vector>

constexpr int maxElement() {
std::vector myVec = {1, 2, 4, 3};
std::sort(myVec.begin(), myVec.end());
return myVec.back();
}
int main() {
constexpr int maxValue = maxElement();

constexpr int maxValue2 = [] {
std::vector myVec = {1, 2, 4, 3};
std::sort(myVec.begin(), myVec.end()) ;
return myVec.back();
}();

std::cout << "maxValue: " << maxValue << '\n';

std::cout << "maxValue2: " << maxValue2 << '\n';
}

consteval (c++20)

consteval只能修饰函数,被consteval修饰的函数必须是编译时运行的,调用consteval函数返回的一定是编译时常量。

  • 不能应用在析构函数上
  • 其它和constexpr一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
consteval int add(int a, int b) {
return a + b;
}

constexpr int add2(int a, int b) {
return a + b;
}

int main() {
constexpr int r = add(1, 2);
constexpr int r2 = add2(1, 2);
int r3 = add(1, 2);
int r4 = add2(1, 2);

int x = 100;
// int r5 = add(x, 2); // error
int r6 = add2(x, 2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int sqrRunTime(int n) { return n * n; };
consteval int sqrCompileTime(int n) { return n * n; }
constexpr int sqrRunOrCompileTime(int n) { return n * n; }

int main() {
constexpr int prod1 = sqrRunTime(100);
constexpr int prod2 = sqrCompileTime(100);
constexpr int prod3 = sqrRunOrCompileTime(100);

int x = 100;
int prd4 = sqrRunTime(x);
int prod5 = sqrCompileTime(x);
int prod6 = sqrRunOrCompileTime(x);
}

constinit (c++20)

constinit与const没有关系,constinit修饰的变量并不是常量,是可以被修改的。constinit保证变量一定是静态初始化的。

static

静态变量分为全局静态变量、局部静态变量、类中静态成员变量。
按照初始化的类型分为静态初始化(static initialization)和动态初始化(dynamic initialization)。

静态初始化

指的是用常量来对静态变量进行初始化,包括zero initialization和const initialization;对于静态初始化的变量,是在程序编译时完成的初始化。

动态初始化

指的是需要调用函数才能完成的初始化,或者是复杂类型的初始化等,对于这种全局静态变量、类的静态成员变量,是在main()函数执行前,加载时调用相应的代码进行初始化的。而对于局部静态变量,是在函数执行至此初始化语句时才开始执行的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
consteval int sqr(int n) {
return n * n;
}

constexpr int sqr2(int n ) {
return n * n;
}

static int sg1 = sqr(100);
static int sg2 = sqr2(10);
static int sg3 = 1;

constinit int g1 = sqr(100);
constinit int g2 = sqr2(100);
constinit int g3 = 1;

int main() {

}

Static Initialization Order Fiasco

static变量如果是动态初始化,那么编译单元之间的static变量初始化的顺序是不确定的。

1
2
3
4
int square(int n) {
return n * n;
}
static int staticA = square(5);
1
2
3
4
5
6
7
8
#include <iostream>

extern int staticA;
static int staticB = staticA; // 使用局部静态变量解决

int main () {
std::cout << staticB;
}

使用constinit来解决static初始化顺序的问题

1
2
3
4
5
constexpr int quad(int n) {
return n * n;
}

constinit int staticA = quad(5);
1
2
3
4
5
6
7
8
#include <iostream>

extern constinit int staticA;
constinit int staticB = staticA;

int main() {
std::cout << staticB;
}

std::is_constant_evaluated (c++20)

判断函数是编译期执行还是运行时执行