const

主要有以下作用:

  1. 修饰变量,说明该变量不可改变;
  2. 修饰指针,这里分为指向常量的指针(指针常量,pointer to const)自身是常量的指针(常量指针,const pointer)
    1
    2
    3
    int a = 0, b = 10;
    const int *p1 = &a; // 指针常量,指向的值不可以使用*p修改,*p = 10 错误,可以修改指向的地址
    int *const p2 = &a; // 常量指针,指针指向的地址不可改变,p2 = &b 错误,可以修改该地址的变量值
  3. 修饰引用,指向常量的引用,用于形参类型,既避免了拷贝,又避免了函数对值进行修改
  4. 类内修饰成员函数,说明该成员函数内不能修改成员变量
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
28
#include <iostream>

class A {
private:
const int a;
public:
// 构造函数
A() : a(0) { };
A(int x) : a(x) { };

// const可用于对重载函数的区分
// 普通成员函数
void func() {
std::cout << "这是一个普通函数" << std::endl;
}
// 常成员函数,不得修改类中任何数据的值
void func() const {
std::cout << "这是一个常函数" << std::endl;
}
};

int main() {
A obj1;
const A obj2;
obj1.func(); // "这是一个普通函数"
obj2.func(); // "这是一个常函数"
return 0;
}

需要注意的是,普通对象可以调用普通函数也可以调用常函数,前者优先,如果是常量对象的话,只能调用常函数,如果调用普通函数会导致报错。

进一步地

1
2
3
4
5
6
7
8
9
10
11
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量
// 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰

// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();

static

主要有以下作用:

  1. 修饰普通变量: 修改变量的存储区域和生命周期,使变量存储在静态区域,在main函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。需要注意的是: 在函数内部定义了一个静态变量,生命周期到程序结束,但是这个变量的作用域仅限于声明它的函数内部。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void myFunction() {
    static int count = 0; // 静态局部变量
    count++;
    std::cout << "Count: " << count << std::endl;
    }

    int main() {
    myFunction(); // 输出 Count: 1
    myFunction(); // 输出 Count: 2
    // 这里无法直接访问 count
    return 0;
    }
  2. 修饰普通函数: 表明函数的作用范围,仅在定义该函数的文件内才能使用,它的作用域被限制在声明它的文件中,即它变成了一个“内部链接”的函数,只能在当前文件内部访问。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
    1
    2
    3
    4
    5
    6
    7
    8
    // File1.cpp
    static void myFunction() {
    std::cout << "这是一个静态函数" << std::endl;
    }
    // File2.cpp
    void anotherFunction() {
    myFunction(); // 错误,无法访问静态函数
    }
  3. 修饰成员变量: 修饰成员变量时该变量将被所有该类的对象共享,而不是每个对象拥有一份副本,而且不需要生成对象就可以访问该成员。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MyClass {
    public:
    static int count; // 静态成员变量
    };

    int MyClass::count = 0; // 静态成员变量的初始化

    int main() {
    MyClass obj1;
    MyClass obj2;

    obj1.count = 5;
    std::cout << obj2.count << std::endl; // 输出 5

    return 0;
    }
  4. 修饰成员函数: 修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内只能访问 static 成员。

this 指针

this 指针是一个特殊的指针,它指向当前对象的地址。在 C++ 中,每个类的非静态成员函数都有一个隐含的 this 指针,它指向调用该成员函数的对象。

当一个类对象调用成员函数时,编译程序先将对象的地址赋给了 this 指针,然后调用该成员函数,每次成员函数存取数据成员时,都其实是在隐式地使用 this 指针。

this 指针是一个常量指针,被隐含地声明为: ClassName *const this ,这意味着不能给 this 指针赋值,而在 const 成员函数里被声明为 const ClassName* const

最后还需要注意的是,this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)

左值和右值(引用)

  1. 左值(lvalue)
  • 左值既能够出现在等号左边,也能出现在等号右边
  • 左值可以被赋值,可以作为赋值语句的目标
  • 左值是可寻址的变量,有持久性
  • 具体来说,变量、对象或者通过解引用获得的指针都属于左值
    1
    2
    int x = 5; // x 是左值,因为它代表一个内存位置,可以被赋值
    int* ptr = &x; // &x 是左值,因为它是变量 x 的地址
  1. 右值(rvalue)
  • 右值是不能被赋值的表达式,它们代表的是一个数值或者临时值,通常在赋值语句的右侧
  • 右值可以是一个常数、一个临时的计算结果或者一个表达式的返回值。
  • 右值在使用后就失去意义,因此不能被取地址
    1
    2
    int y = 10; // 10 是右值,因为它代表一个数值,不能被赋值
    int z = x + y; // x + y 是右值,因为它代表一个临时计算结果

C++ Primer:” 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)”

左值引用(lvalue reference)和右值引用(rvalue reference)是C++中引入的两种不同类型的引用。

  1. 左值引用: 左值引用是最常见的引用类型。它们使用 & 符号声明,并且只能绑定到左值(可以取地址的表达式)。
  2. 右值引用: 右值引用是在C++11中引入的新特性,用 && 符号表示。它们可以绑定到临时值、表达式结果或具有名称的右值。
    1
    2
    3
    4
    5
    6
    7
    int x = 6; // x是左值,6是右值
    int &y = x; // 左值引用,y引用x

    int &z1 = x * 6; // 错误,x*6是一个右值

    int &&z2 = x * 6; // 正确,右值引用
    int &&z3 = x; // 错误,x是一个左值

    可以引用右值的除了右值引用外还有 const 左值引用,例如 const int &z4 = x * 6; 正确,可以将一个const引用绑定到一个右值。例如 std::vectorpush_back 函数就使用了 const 左值引用(void push_back (const value_type& val);) ,这样能让我们使用 v.push_back(1) 这样的代码。

std::move 可以实现将左值转换成右值以实现对左值进行右值引用

1
2
3
int i = 3, j;
j = std::move(2); // 合法,从一个右值移动数据
j = std::move(i); // 合法,从一个左值移动数据,i的值之后是不确定的。

inline 内联函数

主要有以下特征:

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
1
2
3
4
5
6
7
8
9
10
// 一般在类内定义的成员函数会隐式成为inline,而在类外定义的成员函数不会(当然是否内联完全取决于编译器)
// 类内定义,隐式内联
class A {
int doA() { return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() { return 0; } // 需要显式内联

虚函数可以是内联函数吗?
Standard C++: Are “inline virtual” member functions ever actually “inlined”?
Stackoverflow: Can virtual functions be inlined [duplicate]

虚函数可以是内联函数,但是当虚函数表现出多态性的时候不能内联,因为内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性的时候不可以内联。

下面是虚函数内联使用例子

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
28
29
30
31
32
#include <iostream>

class Base {
public:
inline virtual void who() {
std::cout << "I am Base" << std::endl;
}
virtual ~Base() { }
};

class Derived: public Base {
public:
// // 不写 inline 时会隐式内联
inline void who() {
std::cout << "I am Derived" << std::endl;
}
};

int main() {
Base fa;
fa.who(); // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。

// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();

// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;

return 0;
}

具体来说,就是

  1. 当使用类的对象来调用时,则虚函数可以当做是内联的,因为编译器在编译时就确切知道对象是哪个类的;
  2. 当使用基类指针或引用来调用虚函数时,它都不能是内联函数,因为调用发生在运行时,是动态绑定的。