【C++学习笔记】【基础】0.C与C++

🍕阿i索 个人主页
《C语言专栏》 《C++专栏》
《数据结构专栏》 待更新...

前言.

在熟悉了 C 语言的底层逻辑与高效特性后,我们不妨迈出下一步 ------ 走进 C++ 的世界。

从最经典的 hello world 就能直观感受到两者的差异:

了解C++的产生与发展

C++ 诞生于 1983 年,由 Bjarne Stroustrup 在 C 语言基础上扩展而来,核心是引入面向对象编程,解决了 C 在大型项目中的可维护性短板。1998 年 C++98 标准发布标志着语言成熟,而 C++11 则是现代 C++ 的分水岭 ------ 智能指针、Lambda 表达式等特性让开发更简洁安全;后续 C++14/17/20 的迭代,进一步强化了模块化、协程等能力,让它在系统开发、游戏引擎、高性能计算等领域始终占据核心地位。

C++ 在保留 C 语言大部分语法的基础上,针对 C 语言存在的缺陷提供了改进方案,是学习高性能编程的必经之路


📚 学习资源推荐

1.最新更新至 C++11 的头文件形式参考文档(结构清晰,适合入门查阅)

2.C++ 官方英文文档(权威标准,适合深入研究)

接下来,我们就从基础语法开始,一步步探索 C++ 的强大之处。


一. 命名冲突------命名空间

C++通过引入命名空间机制有效解决了全局作用域中的命名冲突问题。该特性允许将变量、函数和类等标识符限定在特定的命名空间内,从而实现名称的本地化管理。

如何定义命名空间?

  1. 使用namespace关键字声明,后跟命名空间名称
  2. 在大括号{}内定义命名空间的成员,可以包含变量、函数、类型等
  3. 命名空间必须定义在全局作用域中,支持嵌套定义

注意:

  • 同一命名空间可以在多个文件中分别定义,系统会自动合并
  • 不同文件中定义的同名命名空间不会产生冲突

命名空间的本质是创建一个独立的作用域,该作用域与全局作用域相互隔离。通过这种方式,不同作用域中可以定义同名的变量,从而有效避免了命名冲突问题。需要注意的是,命名空间内定义的变量仍属于全局变量范畴,只有函数内部或代码块(例如for循环)中定义的变量才是局部变量。

为什么不同的域可以定义同名变量?

C++中域有函数局部域,全局域,命名空间域,类域。域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。

如何访问变量?

变量访问遵循"局部优先"原则,即优先查找局部变量。要访问其他作用域的变量,需使用域作用限定符 " :: "

  • 单独使用 " :: " 表示访问全局变量
  • 前加命名空间名称可访问指定命名空间的变量

调用其他作用域的函数时,需在函数名前添加命名空间名称。例如调用C++标准库函数(位于std命名空间)的格式为:"std::函数名"。

访问嵌套命名空间时,需多次使用域作用限定符。

编译器查找变量的默认顺序:

  1. 局部作用域
  2. 全局作用域
  3. 未找到则报错"未声明的标识符"

若指定了作用域,则直接在该作用域中查找,未找到同样报错。

**命名空间的使用场景:**当一个项目包含大量文件时,函数命名容易产生冲突。通过在头文件中定义命名空间,可以为函数提供区分标识。值得注意的是,不同文件中可以定义同名命名空间,它们会被视为同一个命名空间而不会产生冲突。

如何使用命名空间?

  1. 使用域作用限定符"::":通过指定命名空间来访问成员,这是项目开发中的推荐方式。

  2. 使用using展开整个命名空间:将命名空间中的所有成员都引入当前作用域,适合练习和刷题时使用。

  3. 使用using引入特定成员:只展开命名空间中的某个成员,适合项目中需要频繁访问且不会造成命名冲突的成员。

二. C++的输入输出

C++的输入输出流库是<iostream>(Input Output Stream的缩写)。

在标准库中:

  • std::cin是istream类的对象,负责处理窄字符的标准输入
  • std::cout是ostream类的对象,负责处理窄字符的标准输出
  • std::endl是一个函数,在流操作中相当于插入换行符并刷新缓冲区

主要运算符:

  • << 是流插入运算符

  • >> 是流插入运算符

这些组件都定义在std命名空间中,使用时需要通过命名空间访问。

相比C语言,C++的输入输出更加便捷,能够自动识别变量类型而无需手动指定格式。

一些编译器想要使用printf、scanf需要包含头文件<cstdio>。

三. 缺省参数

缺省参数是指在函数声明或定义时为参数预设默认值。当调用函数时,若未传入对应实参,则自动使用该默认值;若传入实参,则使用传入值。缺省参数分为两种类型:全缺省(所有参数都设默认值)和半缺省(部分参数设默认值)。需要注意的是,C语言不支持缺省参数功能,这是C++特有的特性。

如何使用缺省参数?

  • 声明与定义分离规则 :函数声明和定义分离时,缺省参数不可重复指定,需在声明中指定缺省值(无声明时可在定义中指定)。
  • 半缺省参数规则 :半缺省参数必须从右往左依次连续指定,禁止间隔跳跃给缺省值。
  • 函数调用规则 :调用带缺省参数的函数时,实参需从左到右依次传递,不能跳过前面的参数给后面传值。

使用缺省参数可以去解决的场景:

当Stack需要存储n个元素时,如果预先分配足够的空间,就能避免频繁扩容操作。这种情况下,可以在初始化函数的声明或定义中添加缺省参数来实现。

四. 函数重载

C++允许在同一作用域内定义多个同名函数,但要求它们的形参列表必须有所区别(参数数量或类型不同)。这种函数重载机制使得C++的函数调用展现出多态特性,使用更加灵活。相比之下,C语言严格禁止同一作用域内出现同名函数,因此不支持函数重载功能。

五. 引用

C++引入引用(reference)的概念,本质上是为变量创建别名。例如:"及时雨"和"宋江"都是指同一个人。引用并非定义新变量,而是为现有变量赋予另一个名称。编译器不会为引用分配独立的内存空间,引用与其指向的变量共享相同的内存地址。

引用格式:类型& 引用别名=引用对象

引用的特性:

  1. 引用在定义时必须初始化;
  2. 一个变量可以有多个引用;
  3. 引用一旦引用一个实体,再不能引用其他实体。

引用可以解决大部分指针问题,但在特定场景仍需使用指针(如链表、树等数据结构中的节点定义)。这是因为C++引用一旦初始化就无法改变其指向对象,而某些场景需要动态调整节点指向关系,此时只能使用指针来实现。

引用主要用于传参和返回值场景,能有效减少数据拷贝提升效率。修改引用对象时,原对象也会同步更新。

1. 引用传参

引用传参和指针传参在功能上相似,但引用传参使用起来更为便捷。下面以SeqList为例,分别展示指针传参和引用传参的实现方式:

也有的地方先对结构体地址进行引用:

2. 传引用返回

六. const引用

在 C++ 中,若要引用const对象,必须使用const引用;而const引用也可引用普通(非 const)对象 。这是因为引用在绑定对象时,遵循 权限只能缩小、不能放大 的规则:

  • 权限缩小(允许) :普通对象 → const 引用(限制修改权限)
  • 权限放大(禁止)const 对象 / 临时对象 → 普通引用(试图突破只读限制)

需要特别注意以下两种非法场景,其核心问题均为引用了临时对象

复制代码
int a = 10;
int& rb = a*3;    // 错误:引用临时对象,且未使用const引用
double d = 12.34;
int& rd = d;      // 错误:类型转换产生临时对象,引用权限放大

临时对象的产生原因

  1. a*3是表达式求值,其计算结果会被编译器存储在一个临时对象中(临时对象无名称,仅暂存结果);
  2. int& rd = d中,double类型的d转换为int类型时,会生成临时对象存储转换后的中间值

C++ 标准规定:所有临时对象都具有 "常性"(const 属性) 。上述代码中,rbrd是普通非 const 引用,试图引用具有常性的临时对象,本质是 "权限放大"(普通引用试图修改只读的临时对象),因此编译报错。

正确的写法是使用const引用绑定临时对象:

复制代码
int a = 10;
const int& rb = a*3;    // 正确:const引用可绑定临时对象
double d = 12.34;
const int& rd = d;      // 正确:const引用接受类型转换产生的临时对象

补充定义:临时对象(Temporary Object)是编译器在求值表达式(如算术运算、类型转换)时,为暂存中间结果临时创建的未命名对象,其生命周期通常仅限于当前表达式,且默认具有 const 属性。

const 引用的三大用法

1. 绑定 const 对象

必须使用 const 引用,否则会因权限放大报错:

复制代码
const int x = 10;
const int& rx = x; // ✅ 正确
// int& rx = x;    // ❌ 错误:const对象 → 普通引用(权限放大)

2. 绑定普通对象

const 引用可以安全绑定普通对象,只是限制了修改权限:

复制代码
int y = 0;
const int& ry = y; // ✅ 正确

3. 绑定临时对象(唯一合法方式)

临时对象(字面量、表达式、类型转换产生)默认具有常性 ,只能被 const 引用绑定:

复制代码
const int& r1 = 10;       // ✅ 绑定字面量临时对象
const int& r2 = y * 2;    // ✅ 绑定表达式临时对象
double d = 12.34;
const int& r3 = d;        // ✅ 绑定类型转换临时对象

普通引用的限制

普通(非const)引用 只能绑定可修改的左值(普通变量),不能绑定:

  • const 对象(权限放大)

  • 临时对象(字面量、表达式、类型转换产生)

    int y = 0;
    int& ry = y; // ✅ 正确

    const int x = 10;
    // int& rx = x; // ❌ 错误:const对象 → 普通引用(权限放大)

    // int& r = 10; // ❌ 错误:绑定临时对象(权限放大)
    // int& r2 = y * 2; // ❌ 错误:绑定临时对象(权限放大)

类型转换与临时对象

1. 隐式类型转换

当不同类型绑定引用时,会生成临时对象:

复制代码
int i = 1;
// double& rd = i;  // ❌ 错误:普通引用不能绑定临时对象
const double& rd = i;  // ✅ 正确:const引用绑定转换产生的临时对象
  • intdouble 隐式转换生成临时对象,具有常性。
  • 普通引用绑定临时对象属于权限放大,编译报错;const 引用则合法。

2. 强制类型转换

强制类型转换同样会生成临时对象:

复制代码
int i = 1;
// int& rp = (int)&i;  // ❌ 错误:普通引用不能绑定临时对象
const int& rp = (int)&i;  // ✅ 正确:const引用绑定转换产生的临时对象
  • (int)&iint* 强制转为 int,生成临时对象,具有常性。
  • 只有 const 引用能安全绑定这个临时对象。

函数传参中的引用

1. 不修改实参的情况:推荐用 const 引用

  • 兼容普通对象、const 对象、临时对象,保证安全。

    void func(const int& x) { /* 只读访问 */ }

    int main() {
    const int a = 10;
    int y = 0;
    func(a); // ✅ const对象 → const引用
    func(y); // ✅ 普通对象 → const引用
    func(2); // ✅ 临时对象 → const引用
    return 0;
    }

2. 修改实参的情况:用普通引用

  • 只能接收普通对象,不能接收 const 对象或临时对象。

    void func2(int& x) { x = 100; /* 修改实参 */ }

    int main() {
    int y = 0;
    func2(y); // ✅ 普通对象 → 普通引用

    复制代码
      const int a = 10;
      // func2(a);  // ❌ 错误:const对象 → 普通引用(权限放大)
      // func2(2);  // ❌ 错误:临时对象 → 普通引用(权限放大)
      return 0;

    }

指针与引用

特性 引用(Reference) 指针(Pointer)
内存空间 是变量的 "别名",不开辟空间 存储变量地址,需要开辟空间
初始化要求 定义时必须初始化,不能空引用 建议初始化,允许空指针
指向性修改 初始化绑定对象后,不能更换绑定目标 可随时修改指向,指向不同对象 / 地址
访问目标方式 直接访问( 等价于原变量) 需解引用才能访问指向的对象
sizeof 结果 等于引用类型的大小 (如 int& 是 4 字节) 固定为地址空间大小
安全性 几乎无空 / 野引用问题,使用更安全 易出现空指针、野指针,风险更高

1. 内存与初始化

复制代码
#include <iostream>
using namespace std;

int main() {
    int a = 10;
    
    // 引用:必须初始化,绑定后不能改
    int& ref = a;  // ✅ 正确:引用a
    // int& ref2;   // ❌ 错误:引用未初始化
    
    // 指针:可先定义后初始化,可改指向
    int* ptr;      // ✅ 语法允许:未初始化(不推荐)
    ptr = &a;      // ✅ 指向a
    int b = 20;
    ptr = &b;      // ✅ 改为指向b

    return 0;
}

2. 访问方式与 sizeof

复制代码
#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int& ref = a;
    int* ptr = &a;

    // 访问目标
    ref = 20;          // ✅ 直接修改a的值
    *ptr = 30;         // ✅ 解引用修改a的值

    // sizeof 结果
    cout << "sizeof(ref) = " << sizeof(ref) << endl;  // 4(int类型大小)
    cout << "sizeof(ptr) = " << sizeof(ptr) << endl;  // 8(64位平台地址大小)

    return 0;
}

3. 安全性对比

复制代码
#include <iostream>
using namespace std;

int main() {
    // 指针风险:空指针/野指针
    int* null_ptr = nullptr;
    // *null_ptr = 10;  // ❌ 空指针解引用,程序崩溃

    int* wild_ptr;      // 野指针(未初始化)
    // *wild_ptr = 20;  // ❌ 野指针访问,内存越界

    // 引用安全:无空/野引用
    int a = 10;
    int& ref = a;
    ref = 30;           // ✅ 安全访问

    return 0;
}

使用场景建议:

1. 优先用引用的场景

  • 函数参数传递(避免拷贝,且无需检查空值):void func(int& x)
  • 函数返回值(返回类成员 / 全局变量,避免拷贝):int& getVal()
  • 追求代码简洁、安全的场景

2. 必须用指针的场景

  • 需要动态分配内存(new/delete):int* arr = new int[10]
  • 需要修改指向目标的场景:ptr = &b
  • 实现多态(基类指针指向派生类对象):Base* p = new Derived()

七、宏函数与内联函数

回顾:C 语言宏函数

1. 宏函数的本质

宏是预处理阶段的纯文本替换机制,不是真正的函数调用,无栈帧创建、无类型检查。

2. 宏函数的特点

优点 缺点
1.高效:高频调用小函数时,宏替换避免了函数调用开销,执行效率更高。 2.无栈帧:直接替换代码,不占用栈空间。 1. 极易出错(运算符优先级、重复求值) 2. 无法调试(预处理后代码已展开) 3. 类型不安全(不检查参数类型)

3. 宏函数的安全写法(以 ADD 为例)

复制代码
// 错误版本:多分号+无括号
#define ADD1(a, b) a+b;  // 展开后多余分号,语法错误
// 基础版本:无整体括号,优先级错误
#define ADD2(a, b) a+b   // ADD2(1,2)*3 → 1+2*3=7(错误)
// 安全版本:参数+整体都加括号(推荐)
#define ADD4(a, b) ((a)+(b))  // ADD4(1,2)*3 → ((1)+(2))*3=9(正确)

4. 宏函数建议

  • 每个参数加括号 + 整个表达式加括号,避免优先级问题;
  • 不加分号结尾,由调用者自行添加;
  • 避免带副作用的参数(如ADD(i++, j++)会导致多次自增)。

C++ 内联函数(inline):替代宏函数的更优方案

1. 内联函数的本质

inline修饰的函数,编译阶段 编译器会在调用处直接展开函数体,无需建立栈帧,既保留函数特性,又兼具宏的高效。

2. 内联函数的特点

内联函数 宏函数
编译阶段展开,保留函数特性 预处理阶段文本替换,无函数特性
类型安全(编译检查参数类型) 类型不安全(任意文本均可替换)
可调试(debug 模式默认不展开) 无法调试(预处理后无宏痕迹)
语法简单(无需手动加括号) 语法复杂(易因括号 / 分号出错)
支持函数重载、作用域限制 无重载,全局文本替换

注意

  • inline编译器建议:编译器可拒绝展开(如递归函数、代码量过大的函数);
  • 适用场景:频繁调用的短小函数(如几行代码的加减、判断);
  • 不适用场景:递归函数、代码量大的函数(加inline也会被编译器忽略);
  • 声明与定义不可分离:分离到.h 和.cpp 会导致链接错误(展开后无函数地址);
  • VS 调试说明:debug 版本默认不展开(方便调试),release 版本默认展开(追求效率)。

3. 内联函数的代码示例

复制代码
#include <iostream>
using namespace std;

// 内联函数:替代ADD宏,简洁且安全
inline int Add(int a, int b) {
    return a + b;
}

int main() {
    // 编译时展开为:int ret = 1 + 2; 无栈帧开销
    int ret = Add(1, 2);
    // 无需担心优先级:Add(1,2)*3 → (1+2)*3=9
    int ret2 = Add(1, 2) * 3;
    cout << ret << " " << ret2 << endl; // 输出:3 9
    return 0;
}

4. 内联函数的使用注意

复制代码
// ❌ 错误:声明与定义分离(链接错误)
// test.h 中声明:inline int Add(int a, int b);
// test.cpp 中定义:int Add(int a, int b) { return a+b; }

// ✅ 正确:声明+定义写在.h中(或直接写在调用处)
inline int Add(int a, int b) {
    return a + b;
}

5. 宏函数 vs 内联函数

特性 宏函数(C) 内联函数(C++)
处理阶段 预处理阶段(文本替换) 编译阶段(函数体展开)
类型检查 无(类型不安全) 有(编译检查,类型安全)
调试性 无法调试 可调试(debug 默认不展开)
语法复杂度 高(需手动加括号) 低(普通函数写法)
适用场景 C 语言短小高频函数 C++ 短小高频函数(替代宏)
重载 / 作用域 不支持 支持
递归 / 大函数 可替换(但易出错) 编译器自动忽略 inline

八、NULL 与 nullptr

回顾NULL

NULL 是一个宏,在标准头文件 stddef.h 中定义:

复制代码
#ifndef NULL
  #ifdef __cplusplus
    #define NULL 0          // C++ 中通常定义为字面量 0
  #else
    #define NULL ((void *)0) // C 中定义为无类型指针 (void*)
  #endif
#endif

NULL容易遇见的情况:

(1)重载匹配错误

当函数存在 int 和指针版本的重载时,NULL 会被错误地匹配到 int 版本:

复制代码
void f(int x)      { cout << "f(int x)" << endl; }
void f(int* ptr)   { cout << "f(int* ptr)" << endl; }

int main() {
    f(0);      // ✅ 匹配 f(int x)
    f(NULL);   // ❌ 仍匹配 f(int x),因为 NULL 被定义为 0
    return 0;
}

(2)类型转换歧义

  • f((void*)NULL):在 C++ 中会直接编译报错,因为 void* 无法隐式转换为 int*
  • f((int*)NULL):虽然可以强制转换调用指针版本,但写法繁琐。

C++11 新关键字:nullptr

1. nullptr 的本质

nullptr 是 C++11 引入的特殊关键字 ,也是一种特殊类型的字面量。它的类型是std::nullptr_t ,可以隐式转换为任意指针类型 ,但不能转换为整数类型

2. nullptr 解决的问题

(1)正确匹配指针重载
复制代码
void f(int x)      { cout << "f(int x)" << endl; }
void f(int* ptr)   { cout << "f(int* ptr)" << endl; }

int main() {
    f(nullptr);  // ✅ 正确匹配 f(int* ptr)
    return 0;
}
  • nullptr 只能被当作指针,不会被误判为整数,彻底解决了重载匹配错误。
(2)统一空指针表示
复制代码
int*   p1 = nullptr;  // ✅ 任意指针类型均可接收
char*  p2 = nullptr;
double* p3 = nullptr;
  • 无需强制类型转换,代码更简洁、安全。

NULL对比nullptr

特性 NULL (C++) nullptr (C++11+)
本质 宏,通常被定义为 0 关键字,类型为 std::nullptr_t
类型安全 可隐式转换为整数,易引发重载匹配错误 只能转换为指针类型,类型安全
重载匹配 易匹配到 int 版本,而非指针版本 精确匹配指针版本
代码可读性 语义模糊(是 0 还是空指针?) 语义明确:代表空指针
兼容性 兼容 C/C++ 旧代码 C++11 及以后支持

代码示例:

复制代码
#include <iostream>
using namespace std;

void f(int x)      { cout << "f(int x)" << endl; }
void f(int* ptr)   { cout << "f(int* ptr)" << endl; }

int main() {
    // 1. 字面量 0
    f(0);          // 输出: f(int x)

    // 2. NULL 的问题
    f(NULL);       // 输出: f(int x) ❌ 错误匹配
    // f((void*)NULL); // 编译错误: 无法从 void* 转换为 int*
    f((int*)NULL); // 输出: f(int* ptr) ✅ 强制转换可行,但繁琐

    // 3. nullptr 的正确用法
    f(nullptr);    // 输出: f(int* ptr) ✅ 正确匹配

    // 4. 定义空指针
    int* p1 = NULL;    // 旧写法
    int* p2 = nullptr; // 新写法(推荐)

    return 0;
}

建议在C++11 及以后的代码中,一律使用 nullptr 表示空指针。


下一篇再见🫡

相关推荐
ZHOUPUYU5 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
时代的凡人9 小时前
0208晨间笔记
笔记
fpcc9 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t9 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
今天只学一颗糖9 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
赶路人儿10 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12310 小时前
C++使用format
开发语言·c++·算法
码说AI11 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS11 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化