文章

c++高频总结四

函数重载的机制。重载是在编译期还是在运行期确定

在 C++ 中,函数重载是通过函数的名称、参数数量和参数类型来区分的,函数重载的解析是在编译期完成的。编译器在编译代码时,会根据函数调用时提供的参数类型和数量,匹配一个最佳的函数声明。

详细说明

  1. 编译期解析

    • 在代码编译阶段,编译器会根据函数调用的上下文和可见的函数声明列表,选择匹配的重载函数。
    • 解析过程包括:
      • 查找与函数名相符的候选函数集。
      • 检查参数类型和数量是否与候选函数兼容。
      • 如果有多个兼容的候选函数,编译器会根据重载解析规则选择最佳匹配(例如通过隐式类型转换优先级等规则)。
  2. 不能重载的情况

    • 返回类型

      不同不能区分重载:

      1
      2
      
      int foo(int x);
      double foo(int x); // 错误,返回类型不能区分重载。
      
    • 参数列表完全相同

      时不能重载:

      1
      2
      
      int foo(int x);
      int foo(int y); // 错误,参数列表相同。
      
  3. 运行期没有影响

    • 函数重载一旦解析完成,对应的具体函数地址会被嵌入到可执行文件中。运行期调用的函数地址已经是确定的,不再需要解析。

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void foo(int x) {
    std::cout << "foo(int): " << x << std::endl;
}

void foo(double x) {
    std::cout << "foo(double): " << x << std::endl;
}

int main() {
    foo(10);     // 编译器选择 foo(int)
    foo(3.14);   // 编译器选择 foo(double)
    return 0;
}

在编译期,编译器会根据参数类型选择调用的重载版本。例如:

  • 对于 foo(10),选择 foo(int)
  • 对于 foo(3.14),选择 foo(double)

与运行期相关的特例:虚函数

尽管函数重载是编译期解析的,但虚函数的调用解析是运行期完成的(动态绑定)。这是函数重写(override),而非函数重载(overload)。动态绑定依赖于多态和运行时的虚表(vtable)。

总结

  • 函数重载:编译期完成解析。
  • 虚函数的重写:运行期通过虚表完成解析(动态绑定)。

C++ 中的 重载解析规则 是编译器在编译期根据函数调用的上下文选择最佳匹配函数的一系列步骤和规则。以下是详细的规则说明:


1. 候选函数(Candidate Functions)

编译器会首先从当前作用域中找到所有与调用的函数名匹配的函数声明。这些函数称为候选函数


2. 可行函数(Viable Functions)

从候选函数集中,编译器筛选出那些参数个数匹配参数类型可以通过隐式转换匹配的函数。这些函数称为可行函数

  • 参数个数必须与调用一致(或通过默认参数补齐)。
  • 每个参数类型必须能通过以下隐式转换之一适配:
    1. 精确匹配(Exact Match)。
    2. 常量转换(比如将 int 转为 const int)。
    3. 引用绑定。
    4. 标准类型提升(Standard Promotion,比如 intdouble)。
    5. 标准类型转换(Standard Conversion,比如 floatint)。
    6. 用户定义的类型转换(通过 explicit 构造函数或类型转换运算符)。

如果没有任何可行函数,编译器会报错(通常为 “no matching function”)。


3. 最佳匹配规则

如果有多个可行函数,编译器会根据以下规则选择一个“最佳匹配”:

(1) 匹配程度的优先级

  1. 精确匹配(Exact Match)优先于转换匹配。
  2. 标准类型提升(Promotion)优先于标准类型转换(Conversion)。
  3. 标准类型转换优先于用户定义的类型转换。

例子:

1
2
3
4
5
void func(int x);
void func(double x);

func(42);  // 调用 func(int),因为 42 是精确匹配 int。
func(3.14f);  // 调用 func(double),因为 float -> double 是标准提升。

(2) 常量性与引用性匹配

  • 如果调用的参数是常量,则优先匹配常量参数。
  • 如果调用的参数是临时对象或右值,则优先匹配右值引用。

例子:

1
2
3
4
5
6
7
void func(int& x);
void func(const int& x);

int a = 10;
func(a);  // 调用 func(int&),因为 a 是非 const 左值。

func(42);  // 调用 func(const int&),因为 42 是右值,不能绑定到 int&。

(3) 默认参数的考虑

  • 如果两个函数都可以匹配,但一个函数需要使用默认参数,另一个函数不需要,则选择不使用默认参数的函数。

例子:

1
2
3
4
void func(int x, int y = 0);
void func(int x);

func(10);  // 调用 func(int x),因为它不需要默认参数。

(4) 重载歧义解决

当两个函数都同样匹配时,编译器会报“重载歧义”错误。此时需要手动修改代码(比如通过类型转换或提供额外信息)来消除歧义。

例子:

1
2
3
4
5
void func(int x);
void func(float x);

func(10);  // 调用 func(int),因为 10 是精确匹配 int。
func(3.14);  // 报错,double 到 int 和 float 都是标准转换,没有优先级。

解决方法:

1
func(static_cast<float>(3.14));  // 明确调用 func(float)。

(5) 模板函数与非模板函数

  • 如果模板函数与非模板函数都匹配,非模板函数优先。
  • 如果多个模板函数匹配,编译器会根据模板特化规则选择更具体的模板实例。

例子:

1
2
3
4
5
6
void func(int x);
template <typename T>
void func(T x);

func(42);  // 调用非模板版本 func(int),因为非模板函数优先。
func(3.14);  // 调用模板版本 func(T),因为没有非模板版本匹配 double。

(6) 构造函数的特殊规则

  • 当调用重载的构造函数时,编译器会应用与普通函数相同的解析规则。
  • 如果有多个构造函数能匹配,会根据上述规则选择最佳匹配。

例子:

1
2
3
4
5
6
7
struct MyClass {
    MyClass(int x) {}
    MyClass(double x) {}
};

MyClass obj(42);  // 调用 MyClass(int),因为 42 精确匹配 int。
MyClass obj2(3.14);  // 调用 MyClass(double),因为 double 精确匹配。

4. 错误情况

  • 无匹配函数:如果没有可行函数,编译器报错。
  • 歧义错误:如果有多个最佳匹配(相同优先级),编译器报“重载歧义”错误。

5. 总结:

  1. 找到所有候选函数。
  2. 筛选出可行函数。
  3. 根据匹配规则选择最佳匹配函数。
  4. 如果没有匹配或存在歧义,报错。

以上规则保证了重载函数解析的一致性,同时也提供了很大的灵活性。

指针常量和常量指针

在 C++ 中,指针常量(constant pointer)和常量指针(pointer to constant)是两种不同的概念,它们的区别主要在于对指针本身和指针指向的内容的修改限制。以下是详细的解释和举例:


1. 常量指针(Pointer to Constant)

定义:指向常量的指针,即指针指向的内容是只读的,不能通过该指针修改内容,但指针本身可以改变指向其他地址。

语法

1
2
const 类型* 指针名;
类型 const* 指针名;

两种写法等价,const 可以放在类型的左边或右边。

特点

  • 指针指向的内容不可修改。
  • 指针本身的值(即指向的地址)可以改变。

例子

1
2
3
4
5
6
7
int x = 10;
int y = 20;

const int* ptr = &x;  // ptr 是一个常量指针,指向 x。

*ptr = 20;  // 错误:不能通过 ptr 修改指向内容。
ptr = &y;   // 正确:可以改变 ptr 的指向。

关键点

  • *ptr 是只读的,无法通过指针修改内容。
  • 可以重新赋值指针(即修改指向)。

2. 指针常量(Constant Pointer)

定义:常量化的指针,即指针本身是常量,不能修改其指向,但可以通过指针修改指向的内容。

语法

1
类型* const 指针名;

特点

  • 指针本身的值(即指向的地址)不可修改。
  • 指针指向的内容可以被修改(如果未加 const)。

例子

1
2
3
4
5
6
7
int x = 10;
int y = 20;

int* const ptr = &x;  // ptr 是一个指针常量,指向 x。

*ptr = 20;  // 正确:可以通过 ptr 修改指向内容。
ptr = &y;   // 错误:不能修改 ptr 的指向。

关键点

  • ptr 是只读的,不能修改指针指向的地址。
  • 可以修改指向的内容(如果没有 const 限制)。

3. 常量指针常量(Constant Pointer to Constant)

定义:既是常量指针,又是指针常量,指针本身和指针指向的内容都不可修改。

语法

1
const 类型* const 指针名;

特点

  • 指针本身的值(指向的地址)不可修改。
  • 指针指向的内容也不可修改。

例子

1
2
3
4
5
int x = 10;
const int* const ptr = &x;  // ptr 是一个常量指针常量。

*ptr = 20;  // 错误:不能通过 ptr 修改指向内容。
ptr = &x;   // 错误:不能修改 ptr 的指向。

对比总结

类型指针是否可变指向的内容是否可变
常量指针可变不可变
指针常量不可变可变(如果未加 const
常量指针常量不可变不可变

练习

代码片段 1

1
2
3
4
int x = 5;
const int* p = &x;
*p = 10;  // 错误
p = nullptr;  // 正确

代码片段 2

1
2
3
4
int y = 10;
int* const p = &y;
*p = 20;  // 正确
p = nullptr;  // 错误

代码片段 3

1
2
3
4
int z = 30;
const int* const p = &z;
*p = 40;  // 错误
p = nullptr;  // 错误

通过这三种情况,可以理解指针常量、常量指针及其组合的不同限制。

vector的原理,怎么扩容

在 C++ 中,std::vector 是一种动态数组,它提供了自动管理内存的能力,当需要容纳更多元素时会自动扩容。下面详细解释其原理以及扩容机制。


1. std::vector 的原理

std::vector 的核心是一个动态分配的数组,其主要特性包括:

  • 支持随机访问。
  • 元素存储在一块连续的内存中。
  • 自动扩展大小以容纳更多元素。

关键数据成员

一个典型的 std::vector 实现可能包含以下数据成员:

  1. 指向数据的指针(动态数组的首地址)。
  2. 已用容量(size):当前存储的元素数量。
  3. 总容量(capacity):已分配的内存大小(可以存储的最大元素数量)。

2. 扩容机制

std::vector 添加新元素且当前容量不足时,会触发扩容操作,具体步骤如下:

扩容步骤

  1. 分配更大的内存块
    • 新容量通常是当前容量的 2 倍(虽然这不是标准规定,但大多数实现采用此策略,比如 GNU 和 MSVC)。
    • 这种策略通过指数增长减少了扩容的频率,从而优化性能。
  2. 拷贝旧数据
    • 将旧数组中的所有元素拷贝到新分配的内存块中。
  3. 释放旧内存
    • 释放旧数组占用的内存空间。
  4. 更新内部指针
    • 更新指向数据的指针,使其指向新分配的内存块。

扩容性能

扩容是一个代价较高的操作,因为需要重新分配内存并拷贝所有现有元素。然而,由于扩容采用指数增长,实际发生扩容的次数较少,因此摊销的时间复杂度仍为 O(1)(均摊常数时间)。


3. 代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
        std::cout << "Size: " << vec.size() 
                  << ", Capacity: " << vec.capacity() << std::endl;
    }

    return 0;
}

输出示例

1
2
3
4
5
6
7
Size: 1, Capacity: 1
Size: 2, Capacity: 2
Size: 3, Capacity: 4
Size: 4, Capacity: 4
Size: 5, Capacity: 8
Size: 6, Capacity: 8
...

4. 容量管理函数

std::vector 提供了多种与容量管理相关的函数:

capacity()

  • 返回当前分配的总容量(可以存储的最大元素数)。

size()

  • 返回当前存储的元素数量。

reserve(size_type n)

  • 提前为 n 个元素分配内存,但不会改变 size() 的值。
  • 如果 n > capacity(),触发扩容;否则不执行任何操作。

示例:

1
2
std::vector<int> vec;
vec.reserve(100);  // 预分配容量为 100

shrink_to_fit()

  • 请求释放多余的内存,使容量等于当前大小(实现可以选择不执行)。

5. 扩容的实现细节

扩容通常由 STL 实现的 push_back()emplace_back() 触发。伪代码如下:

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
void push_back(const T& value) {
    if (size >= capacity) {
        // 1. 计算新的容量
        size_t new_capacity = capacity == 0 ? 1 : capacity * 2;
        
        // 2. 分配新内存
        T* new_data = allocate(new_capacity);
        
        // 3. 拷贝旧元素到新内存
        for (size_t i = 0; i < size; ++i) {
            new_data[i] = std::move(data[i]);
        }
        
        // 4. 释放旧内存
        deallocate(data, capacity);
        
        // 5. 更新指针和容量
        data = new_data;
        capacity = new_capacity;
    }
    
    // 插入新元素
    data[size] = value;
    ++size;
}

6. 扩容的注意事项

  1. 频繁扩容的代价
    • 如果频繁调用 push_back(),可能会造成多次扩容,影响性能。
    • 使用 reserve() 可以避免频繁扩容。
  2. 容量管理的最佳实践
    • 如果能预估元素数量,建议使用 reserve() 提前分配容量。
    • 如果需要节省内存,可以调用 shrink_to_fit()
  3. 拷贝代价
    • 扩容时拷贝所有元素可能会导致性能瓶颈,尤其是当存储的元素类型比较复杂时。
    • 使用移动语义(C++11)可以显著降低拷贝的代价。

7. 总结

  • 自动扩容机制std::vector 通过倍增容量来减少内存分配和拷贝的频率,从而优化性能。

  • 扩容代价:扩容涉及重新分配内存和拷贝数据,因此尽量使用 reserve() 预分配内存。

  • 时间复杂度

    • 单次扩容:O(n),其中 n为当前元素数量。
    • 均摊复杂度:O(1),扩容的次数随元素数量增长而减少。

介绍一下const

在 C++ 中,const 是一个关键字,用来表示不可修改的内容。它广泛应用于变量、指针、函数、成员函数等场景,既可以提高代码的可读性,也可以帮助编译器进行优化。以下是 const 的详细介绍:


1. const 的基本用法

(1)修饰变量

const 修饰变量时,该变量的值在初始化后就不能被修改。

例子

1
2
const int x = 10;  // x 是一个常量,值为 10
x = 20;            // 错误:x 是只读的

注意

  • const 变量必须在声明时初始化,否则会报错。

(2)修饰指针

在指针相关场景中,const 可以修饰指针本身或指针指向的内容,甚至两者同时修饰。

a. 指针指向的内容是常量

语法:

1
2
const 类型* 指针名;
类型 const* 指针名;

含义:指针指向的内容不能通过该指针修改,但指针本身可以改变指向。

例子

1
2
3
4
int x = 10, y = 20;
const int* ptr = &x;  // 指向 x,但不能修改 x 的值
*ptr = 30;            // 错误:不能通过 ptr 修改指向内容
ptr = &y;             // 正确:可以改变指针的指向
b. 指针本身是常量

语法:

1
类型* const 指针名;

含义:指针本身的值(地址)不能修改,但可以通过指针修改指向的内容。

例子

1
2
3
4
int x = 10, y = 20;
int* const ptr = &x;  // ptr 本身是常量,指向 x
*ptr = 30;            // 正确:可以通过 ptr 修改 x 的值
ptr = &y;             // 错误:不能修改 ptr 的指向
c. 指针本身和指向的内容都是常量

语法:

1
const 类型* const 指针名;

含义:指针本身和指向的内容都不可修改。

例子

1
2
3
4
int x = 10;
const int* const ptr = &x;  // ptr 和 *ptr 都是只读的
*ptr = 20;                  // 错误:不能通过 ptr 修改内容
ptr = &y;                   // 错误:不能修改 ptr 的指向

(3)修饰函数的返回值

a. 返回值是常量

如果函数返回一个 const 值,调用者不能修改该值。

例子

1
2
3
4
5
6
7
8
9
const int func() {
    return 10;
}

int main() {
    int x = func();
    x = 20;  // 正确:x 是一个普通变量
    func() = 20;  // 错误:不能修改函数返回的常量值
}
b. 返回值是指针
  • const int*:返回的指针指向的内容不可修改。
  • int* const:返回的指针本身不可修改。

(4)修饰函数参数

在函数声明中使用 const 可以限制函数对参数的修改。

例子

1
2
3
4
5
6
7
void func(const int x) {
    x = 20;  // 错误:不能修改 x 的值
}

void func(const int* ptr) {
    *ptr = 30;  // 错误:不能通过 ptr 修改内容
}

(5)修饰类成员函数

在类中,const 成员函数表示该函数不能修改类的成员变量(除非这些变量被标记为 mutable)。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
private:
    int x;
public:
    void setX(int val) {
        x = val;  // 正常修改成员变量
    }

    int getX() const {  // const 成员函数
        return x;  // 正常读取成员变量
        // x = 20;  // 错误:不能修改成员变量
    }
};

使用场景

  • const 成员函数主要用于不需要修改类状态的操作,例如 get 方法。
  • 如果函数没有声明为 const,它就不能被 const 对象调用。

(6)修饰类成员变量

类中的 const 成员变量是只读的,必须通过初始化列表在构造函数中初始化。

例子

1
2
3
4
5
6
class MyClass {
private:
    const int x;
public:
    MyClass(int val) : x(val) {}  // 必须通过初始化列表初始化 x
};

2. const 的优势

  1. 提高代码安全性
    • 防止无意中修改变量或对象。
    • 提高代码的可读性,明确哪些值是只读的。
  2. 帮助编译器优化
    • 编译器可以对 const 值进行优化,例如将其内联到代码中。
  3. 兼容性
    • 在函数参数中使用 const 可以兼容常量和非常量对象。

3. constconstexpr 的区别

  • const
    • 表示变量的值在运行时不可修改。
    • 值可以在运行时初始化。
  • constexpr
    • 表示变量的值在编译时已知(常量表达式)。
    • 必须在编译期初始化。

例子

1
2
const int x = 10;         // 值在运行时确定
constexpr int y = 20;     // 值在编译期确定

4. 常见错误和注意事项

  1. 未初始化 const 变量

    1
    
    const int x;  // 错误:必须初始化
    
  2. 与指针结合的语法易混淆

    1
    2
    3
    
    int* const ptr;      // 指针是常量,不能修改指针本身
    const int* ptr;      // 指针指向的内容是常量
    const int* const ptr; // 指针本身和指向的内容都是常量
    
  3. const 成员函数无法调用非 const 成员函数

    1
    2
    3
    
    void constFunc() const {
        nonConstFunc();  // 错误:const 函数不能调用非 const 函数
    }
    

总结

const 是 C++ 中非常重要的关键字,灵活运用 const 可以显著提高代码的安全性、可读性和可维护性。在使用时需要注意与指针、函数和类成员的结合规则。

引用和指针的区别

在 C++ 中,引用指针都是用于操作内存中变量的机制,但它们的行为和使用方式有显著的区别。以下是两者的详细对比:


1. 概念

引用(Reference)

引用是某个变量的别名,一旦绑定到变量,就无法更改绑定。

1
2
3
int a = 10;
int& ref = a;  // ref 是 a 的别名
ref = 20;      // 等价于 a = 20

指针(Pointer)

指针是一个变量,用于存储另一个变量的地址。可以修改指针的指向。

1
2
3
int a = 10;
int* ptr = &a;  // ptr 存储 a 的地址
*ptr = 20;      // 修改 ptr 指向的内容,等价于 a = 20

2. 主要区别

特性引用指针
是否需要初始化必须初始化,绑定后不能改变指向。可以声明不初始化,稍后赋值。
绑定对象引用一旦绑定,不能再绑定其他对象。指针可以随时指向另一个变量或地址。
是否有地址本身没有独立地址,和引用的变量共享地址。指针是一个独立的变量,有自己的地址。
空值引用不能为 null指针可以为 nullptr 或空指针。
访问方式通过引用直接访问对象,语法简洁。需要通过解引用操作符 * 访问对象内容。
指向常量可以引用常量,但不能通过它修改常量。可以指向常量,但需要显式声明 const
数组支持不支持指向数组。可以指向数组,进行遍历或操作。
运算支持无法执行算术运算。可以执行算术运算(如加减偏移量)。

3. 引用和指针的使用场景

(1)引用的使用场景

引用更适合以下场景:

  • 函数参数传递:避免拷贝大对象,且使用语法直观。
  • 函数返回值:用于返回对象的别名,避免拷贝。
  • 操作容器中的对象:如遍历 std::vector

示例:函数参数传递

1
2
3
4
5
6
7
8
void modify(int& ref) {
    ref = 42;  // 修改引用的内容
}

int main() {
    int a = 10;
    modify(a);  // a 被修改为 42
}

(2)指针的使用场景

指针更适合以下场景:

  • 动态分配内存:例如通过 newmalloc 分配。
  • 数据结构实现:如链表、树等需要存储地址的结构。
  • 遍历数组:通过指针偏移操作访问数组元素。
  • 指向可能为空的对象:如动态对象。

示例:动态内存分配

1
2
3
int* ptr = new int(10);  // 动态分配一个整数
std::cout << *ptr << std::endl;  // 输出 10
delete ptr;  // 释放内存

4. 引用和指针的对比细节

(1)初始化

  • 引用必须在声明时初始化:

    1
    2
    
    int a = 10;
    int& ref = a;  // 必须初始化
    

    不允许:

    1
    
    int& ref;  // 错误:引用必须初始化
    
  • 指针可以声明后再初始化:

    1
    2
    3
    
    int* ptr;   // 未初始化的指针
    int a = 10;
    ptr = &a;   // 初始化指针
    

(2)绑定的可变性

  • 引用一旦绑定到某个变量,无法再绑定其他变量:

    1
    2
    3
    
    int a = 10, b = 20;
    int& ref = a;
    ref = b;  // 修改 a 的值为 b,而不是重新绑定 ref
    
  • 指针可以随时改变指向:

    1
    2
    3
    
    int a = 10, b = 20;
    int* ptr = &a;
    ptr = &b;  // 改变 ptr 的指向
    

(3)nullptr 和空引用

  • 引用不能为 nullptr,必须绑定到有效对象。
  • 指针可以为 nullptr,表示它不指向任何对象。

示例:空指针

1
2
int* ptr = nullptr;  // 合法
int& ref = *ptr;     // 错误:引用不能为 nullptr

(4)运算支持

  • 引用不能执行算术运算。
  • 指针可以进行算术运算(如偏移操作)。

示例:指针的偏移

1
2
3
int arr[] = {1, 2, 3};
int* ptr = arr;
std::cout << *(ptr + 1) << std::endl;  // 输出 2

5. 结合使用引用和指针

在某些情况下,引用和指针可以结合使用。例如,函数返回指针的引用。

示例

1
2
3
4
5
int x = 10;
int* ptr = &x;

int*& ref = ptr;  // 引用一个指针
*ref = 20;        // 修改 x 的值为 20

6. 总结

特性引用指针
是否必须初始化
是否可更改指向
是否可以为 null
是否有地址
是否支持运算

选择引用还是指针的准则

  • 如果确定某对象始终有效且引用不会改变,使用引用
  • 如果需要动态管理内存、改变指向,或者需要表示空值,使用指针
本文由作者按照 CC BY 4.0 进行授权