C++ 入门核心知识点总结

C++ 入门核心知识点总结

命名空间 namespace

命名空间的概念

在 C 语言中,当我们编写大程序的时候,多个库之间可能会出现函数名或变量名冲突的问题。为了解决这个问题,C++ 引入了命名空间的概念。

命名空间通过将相关的函数、变量和类封装在一个独立的作用域中,避免了命名冲突。例如:

// 两个库都定义了print函数
// 库A
void print() { /* 打印到控制台 */ }

// 库B
void print() { /* 打印到文件 */ }

// 使用时产生冲突
print(); // 调用哪个?

而我们利用命名空间就可以很好地解决这个问题:

void print() {
    std::cout << "Hello, World!" << std::endl;
}

namespace ljx {
    void print() {
        std::cout << "Hello from ljx namespace!" << std::endl;
    }
    void test() {
        print();  // Calls ljx::print
    }
}

int main() {
    ljx::test();
    return 0;
}

在上面的例子中,我们定义了一个名为 ljx 的命名空间,并在其中定义了一个 print 函数。这样,当我们调用 ljx::print() 时,就不会与全局的 print 函数冲突。

如需要访问全局的 print 函数,可以使用作用域解析运算符 ::

void print() {
    std::cout << "Hello, World!" << std::endl;
}

namespace ljx {
    void print() {
        std::cout << "Hello from ljx namespace!" << std::endl;
    }
    void test() {
        ::print();  // Calls global print
    }
}

int main() {
    ljx::test();
    return 0;
}

而当我们在某一个场景中对命名空间中的所有成员都需要使用时,可以使用 using 声明:
举个常用的例子,当我们初学 C++ 时,常常会使用 std 命名空间中的标准库函数和对象。为了避免每次都写 std:: 前缀,我们可以使用 using 声明:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl; // 直接使用cout和endl
    return 0;
}

需要注意的是,过度使用 using namespace 可能会引入命名冲突,尤其是在大型项目中。因此,建议在头文件中避免使用 using namespace,而在源文件中谨慎使用。

在大项目中使用 using namespace 功能基本可以判定为对项目不负责任的表现,建议使用命名空间限定符来明确指定所使用的命名空间成员。

嵌套命名空间

C++11 引入了嵌套命名空间的概念,允许我们在一个命名空间内部定义另一个命名空间。这有助于更好地组织代码。例如:

namespace Outer {
    int x = 1;

    namespace Inner {
        int y = 2;
        void display() {
            std::cout << x << ", " << y << std::endl;
        }
    }
}

// 访问方式
Outer::Inner::display();

有些时候,我们难免会遇到一个命名空间中又嵌套了很多层命名空间的情况,这时可以使用 C++17 引入的简化语法:

namespace Outer::Inner {
    int y = 2;
    void display() {
        std::cout << "Hello from Outer::Inner!" << std::endl;
    }
}
Outer::Inner::display();

这样看起来会更加简洁明了。

匿名命名空间

匿名命名空间是一种特殊的命名空间,它没有名字。匿名命名空间中的成员只能在定义它们的文件中访问,类似于 static 关键字的作用。使用匿名命名空间可以避免命名冲突,同时限制了成员的作用域。例如:

namespace {
    void secretFunction() {
        std::cout << "This is a secret function!" << std::endl;
    }
}
int main() {
    secretFunction(); // 可以调用
    return 0;
}

这个使用场景相对较少见,但在需要限制函数或变量的作用域时非常有用。

缺省参数

缺省参数允许我们在函数声明时为某些参数指定默认值。如果调用函数时没有提供这些参数的值,编译器会使用默认值。例如:

#include <iostream>
void greet(std::string name = "Guest") {
    std::cout << "Hello, " << name << "!" << std::endl;
}
int main() {
    greet();           // 使用默认参数 "Guest"
    greet("Alice");   // 使用提供的参数 "Alice"
    return 0;
}

而缺省参数的定义是有一些规则的:

  • 缺省参数必须从右向左依次定义,不能在中间参数定义缺省值而左侧参数没有定义缺省值。
void func(int a, int b = 10); // 正确
void func(int a = 5, int b);   // 错误,a在b的左侧
  • 缺省参数只能在函数声明中指定一次,不能在函数定义中再次指定。
void func(int a = 5); // 正确
void func(int a) {    // 正确
    // 函数体
}
  • 如果函数有多个缺省参数,调用时可以省略右侧的参数,但必须从右向左依次省略。这条规则和上面的第一条规则是一致的。
void func(int a = 1, int b = 2, int c = 3); // 定义
func();          // 使用默认值 a=1, b=2, c=3
func(10);        // 使用 a=10, b=2, c=3
func(10, 20);    // 使用 a=10, b=20, c=3
func(10, 20, 30); // 使用 a=10, b=20, c=30

若一个函数的所有参数都有缺省值,我们一般称之为“全缺省参数函数”。反之只定义了部分参数的缺省值,则称之为“部分缺省参数函数”,也称“半缺省参数函数”。

函数重载

函数重载是 C++ 中的一种重要特性,允许我们在同一个作用域内定义多个同名但参数列表不同的函数。编译器会根据函数调用时传递的参数类型和数量来决定调用哪个版本的函数。例如:

#include <iostream>
void print(int i) {
    std::cout << "Integer: " << i << std::endl;
}
void print(double d) {
    std::cout << "Double: " << d << std::endl;
}
void print(std::string s) {
    std::cout << "String: " << s << std::endl;
}
int main() {
    print(42);          // 调用 print(int)
    print(3.14);       // 调用 print(double)
    print("Hello");    // 调用 print(std::string)
    return 0;
}

在上述例子中,我们定义了三个名为 print 的函数,但它们的参数类型不同。编译器会根据传递的参数类型来选择合适的函数进行调用。

当函数满足一下条件,都可以构成重载:

  • 参数个数不同。即使参数类型相同,但参数个数不同,也可以构成重载。
  • 参数类型不同。即使参数个数相同,但参数类型不同,也可以构成重载。
  • 参数顺序不同。即使参数个数和类型相同,但参数顺序不同,也可以构成重载。

当然,还有很多情况也是可以构成重载的,我们其实并不需要去刻意记他们,当两个函数可以构成重载的时候,它们的函数签名一定是不一样的

好比说:

  • const 修饰的常量值和非常量值并不能构成重载。因为它们的函数签名是一样的。
// 函数1:非 const 值传递
void func(int a) {}
// 函数2:const 值传递(与函数1签名相同,编译报错)
void func(const int a) {} // error: redefinition of 'void func(int)'

它们的函数签名都为:_Z4funci。其中:

  • _Z:C++ 函数符号前缀;

  • 4:函数名 func 的长度;

  • func:函数名;

  • i:参数类型 int(const int 仍编码为 i)。

  • 若 const 修饰的是引用或指针类型,则可以构成重载。因为它们的函数签名是不同的。

// 函数1:非 const 指针(指向 int)
void func(int* p) {}
// 函数2:const 指针(指向 const int)
void func(const int* p) {} // 合法重载,签名不同

其中;

函数声明 签名编码 编码拆解
void func(int*) _Z4funcPi Pi = Pointer to int(int*)
void func(const int*) _Z4funcPKi PKi = Pointer to const int(const int*)

const 修饰值属于顶层 const,而 const 修饰引用或指针属于底层 const。顶层 const 不影响函数签名,而底层 const 会影响函数签名。

其实我们不必刻意去理解这些编码规则,你只需要记住,当你给函数传递某个参数时,发现你定义的两个函数都满足这个参数的调用条件,那就意味着这两个函数的签名是一样的,编译器就会报重定义错误,因此也就无法构成重载。

为什么 C 语言不支持函数重载?

C 不支持函数重载,而 C++ 支持,是因为:

C 语言的符号表按照原始函数名生成符号,不进行重整(name mangling)。
C++ 语言在编译阶段对函数名进行重整,使每个重载函数拥有唯一符号名,从而让链接器可以区分它们。

也就是说:
差异发生在“编译 → 生成汇编 / 符号表”这一阶段,最终体现在“链接阶段能否区分不同函数”。

C 和 C++ 的编译过程可以分为 4 个主要阶段:

① 预处理(Preprocessing)

在这一阶段:

  • 展开宏
  • 处理 #include
  • 处理 #define
  • 删除注释

此阶段不涉及语言语义,也不解析重载,因此 C 和 C++ 都一样。


② 编译(Compilation)

这一阶段会进行:

  • 语法分析
  • 语义分析
  • 类型检查
  • 生成汇编代码
  • 为函数生成符号名

核心差异出现在这里:函数名(symbol name)生成方式不同


关键点:C vs C++ 的“符号名生成规则”不同
★ C 语言:函数名不重整(No name mangling)

例如:

int add(int a, int b);
double add(double a, double b);

C 会认为你定义了两个叫 add 的函数 → 编译器报错

因为 C 生成的符号都是一样的:

add
add

会导致链接阶段无法区分。


★ C++:函数名会被重整(Name Mangling)

C++ 编译器会把函数签名编码到符号名里:

int add(int a, int b);
double add(double a, double b);

生成的符号可能是:

_Z3addii        → add(int, int)
_Z3adddd        → add(double, double)

每个重载函数都有 不同的符号名,链接器不需要懂语言规则,只需要匹配二进制符号即可。

能够支持函数重载的根本是 C++ 在编译阶段做了 name mangling


③ 汇编(Assembly)

这一阶段把中间表示翻译成汇编:

  • 指令
  • 寄存器分配
  • 符号引用

C 和 C++ 都生成汇编,但符号名不一样:

C 代码:

call add

C++ 代码:

call _Z3addii
call _Z3adddd

④ 链接(Linking)

链接器做的事情十分简单:

  • .o 文件里的符号引用与符号定义匹配
  • 合并代码段/数据段
  • 生成最终的可执行文件

但它 不懂重载、不懂类型系统
它只做 “名字匹配”。

引用

引用(Reference)是 C++ 中的一种重要特性,它为变量提供了一个别名,这意味着,当我们修改这个别名时,实际上修改的是原变量。引用必须在定义时初始化,并且一旦绑定到一个变量,就不能再绑定到其他变量。引用的语法使用 & 符号。例如:

#include <iostream>
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10;
    int y = 20;
    swap(x, y); // 传递引用
    std::cout << "x: " << x << ", y: " << y << std::endl; // 输出 x: 20, y: 10
    return 0;
}

若我们在 C 语言中实现类似的功能,则需要使用指针:

#include <stdio.h>
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int x = 10;
    int y = 20;
    swap(&x, &y); // 传递指针
    printf("x: %d, y: %d\n", x, y); // 输出 x: 20, y: 10
    return 0;
}

引用在底层也是通过指针实现的。但是频繁的指针解引用和取地址操作使得代码变得冗长且易出错,而引用则提供了更简洁和安全的方式来实现类似的功能。

引用特性

1、引用必须在定义时初始化,且不能为 nullptr

int &ref; // 错误,引用必须初始化
int a = 10;
int &ref = a; // 正确

2、一个变量可以有多个引用,但引用本身不能重新绑定到其他变量。

int a = 10;
int &ref1 = a;
int &ref2 = a; // ref1 和 ref2 都引用 a
int b = 20;
ref1 = b; // 这不是重新绑定,而是将 b 的值赋给 a

常引用

常引用(const reference)是指引用的值不能被修改。使用常引用可以防止意外修改数据,同时允许我们引用临时对象或常量。例如:

#include <iostream>
void print(const int &value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    int x = 10;
    print(x);        // 可以传递变量
    print(20);      // 也可以传递临时值
    return 0;
}

下面看一段代码:

#include <iostream>

int main() {
    const int a = 10;
    int& ra = const_cast<int&>(a); // 去掉const属性
    ra = 20; // 修改ra的值,未定义行为
    std::cout << "a: " << a << std::endl; // 输出未定义行为的结果
    return 0;
}

一般来说,我们需要通过 const int& 来引用常量,但是这里利用 C++const_cast 去掉了 aconst 属性,从而使得 ra 成为了一个非常量引用,进而可以修改 a 的值。

这是一种未定义行为,强烈不建议这样做。

引用作为函数返回值

引用也可以作为函数的返回值类型,这样可以避免不必要的拷贝,提高性能。例如:

#include <iostream>
int& getElement(int* arr, int index) {
    return arr[index]; // 返回数组元素的引用
}

此时我们可以直接修改数组元素:

```cpp
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    getElement(arr, 2) = 10; // 修改第三个元素
    std::cout << "arr[2]: " << arr[2] << std::endl; // 输出 arr[2]: 10
    return 0;
}

当然,我们不能将函数栈中的变量作为引用返回,因为栈变量在函数返回后会被销毁,引用将变为悬空引用,导致未定义行为。例如:

int& invalidReference() {
    int x = 10;
    return x; // 错误,返回栈变量的引用
}

内联函数

在 C++ 中,inline 的真正作用常被误解。尽管它的字面含义类似“内联展开”,但现代 C++ 中 inline 的核心意义在于 解决跨文件定义的重复符号问题(ODR:One Definition Rule),而非提供性能优化。

1. inline 的真正意义:允许在头文件中定义函数或变量

历史上,C/C++ 不允许在多个源文件中重复定义同名函数,否则在链接阶段会产生 “multiple definition” 错误。
使用 inline 的函数或(C++17 起)变量,则允许:

  • 在头文件中定义
  • 被多个 .cpp 文件同时包含
  • 链接器会将所有定义折叠成一个

因此 inline 的本质作用是:

使函数或变量在多个翻译单元中具有“弱定义(weak ODR)”,避免重复定义错误。

2. 编译器是否实际“内联展开”与 inline 无关

编译器是否进行真正的“内联优化”(把函数体复制到调用点)完全由优化器决定,而不是由 inline 决定。现代编译器即便没有 inline,也会根据启发式规则自动内联短小函数;而即使加了 inline,编译器也可能拒绝内联(如函数过大、含递归、异常、虚调用等)。

inline 是一个“可以忽略的建议”,不是强制指令。

3. 现代 C++(C++17)扩展:inline 变量

从 C++17 起,inline 可以用于变量:

inline int global_value = 10;

这允许在头文件中定义全局变量、类静态成员变量,而不会产生重复定义,从而统一了函数与变量的定义模型。

4. 实际应用场景

  • 头文件中的模板函数(模板本身需要定义在头文件)
  • 头文件中的短小工具函数
  • C++17 之后:头文件中的全局变量或 static 类成员变量
  • constexpr 函数和模板函数天生具有 inline 属性

auto 关键字

auto 关键字用于类型推断,允许编译器根据初始化表达式自动推导变量的类型。它简化了代码,尤其是在处理复杂类型时。例如:

#include <iostream>
#include <vector>

int main() {
    auto x = 10;               // 推断为 int
    auto y = 3.14;            // 推断为 double
    auto str = "Hello";       // 推断为 const char*
    
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " "; // 使用迭代器
    }
    std::cout << std::endl;
    
    return 0;
}

在上面的例子中,auto 关键字让我们无需显式指定变量的类型,编译器会根据赋值表达式自动推断出正确的类型。

别忘了必要时候带上 &

当我们使用 auto 声明变量时,如果希望变量是引用类型,需要显式地在 auto 后面加上 & 符号。例如:

int a = 10;
auto &ref = a; // ref 是 a 的引用
ref = 20; // 修改 ref 也会修改 a
std::cout << "a: " << a << std::endl; // 输出 a:

否则,auto 会推断为值类型,修改变量不会影响原始变量。

需要注意的是,autoauto* 在声明指针时没有区别,因为指针本身就是一种类型。

范围 for 循环

范围 for 循环(Range-based for loop)是 C++11 引入的一种简化的循环语法,用于遍历容器(如数组、向量等)中的元素。它使代码更加简洁和易读。例如:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用范围 for 循环遍历容器
    for (const auto &value : vec) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

在上面的例子中,范围 for 循环遍历了 vec 容器中的每个元素,并将其赋值给 value 变量。使用 const auto & 可以避免不必要的拷贝,提高性能。