C++学习笔记(一)
6. C++ 的第一个程序
C++ 兼容 C 语言绝大多数语法,因此 C 语言风格的hello world程序在 C++ 中仍可运行,但 C++ 也有专属的输入输出实现,核心差异体现在头文件与输入输出对象的使用上。
6.1 两种实现方式对比
| 实现风格 | 代码示例 | 关键说明 |
|---|---|---|
| C 语言兼容版 | ```cpp | |
| #include<stdio.h> | ||
| int main() | ||
| { | ||
| printf("hello world\n"); | ||
| return 0; | ||
| } | ||
| ``` | 1. 依赖 C 语言标准输入输出头文件<stdio.h>;2. 编译要求:文件后缀需改为.cpp(VS 编译器会调用 C++ 编译器,Linux 环境需用g++编译,而非 C 语言的gcc);3. 本质是复用 C 语言的printf函数实现输出。 | |
| C++ 专属版 | ```cpp | |
| #include<iostream> | ||
| using namespace std; | ||
| int main() | ||
| { | ||
| cout << "hello world\n" << endl; | ||
| return 0; | ||
| } | ||
| ``` | 1. 依赖 C++ 标准输入输出流库头文件<iostream>;2.std是 C++ 标准库命名空间,using namespace std用于简化代码,避免每次使用标准库组件都添加std::前缀;3.cout是 C++ 标准输出对象,<<为流插入运算符,endl的作用是插入换行符并刷新缓冲区。 |
7. 命名空间
命名空间(namespace)是 C++ 为解决 “命名冲突” 问题设计的核心机制,通过将变量、函数、类等标识符封装在独立的作用域中,避免全局作用域内的名字污染。
7.1 命名空间的价值
在 C 语言中,全局作用域下的变量、函数极易发生命名冲突(例如自定义的rand变量与标准库中的rand函数冲突)。C++ 引入命名空间后,通过 “域隔离” 解决此问题:
- 不同命名空间内可定义同名标识符,编译时编译器会按 “作用域” 查找标识符的声明或定义,有效避免冲突;
- C++ 标准库(如
cout、vector等)全部封装在std(standard)命名空间中。
7.2 命名空间的定义规则
- 基本定义:使用
namespace关键字 + 命名空间名 + 花括号{},花括号内可定义变量、函数、结构体等成员,本质是创建一个独立的作用域。cpp
运行
// 以项目名“bit”作为命名空间名,实际开发中可使用项目名或个人缩写 namespace bit { int rand = 10; // 与标准库rand函数不冲突 int Add(int left, int right) { return left + right; } // 命名空间内可定义结构体 struct Node { int val; struct Node* next; }; } - 嵌套定义:命名空间支持嵌套,可用于解决多层级的命名冲突问题。
cpp
运行
namespace bit { // 子命名空间pg namespace pg { int rand = 1; int Add(int left, int right) { return left + right; } } // 子命名空间hg namespace hg { int rand = 2; int Add(int left, int right) { return (left + right) * 10; } } } - 多文件合并特性:不同文件中定义的同名命名空间,编译器会自动将其合并为一个。例如在
Stack.h和Queue.h中分别定义的namespace bit,最终会被视为同一个命名空间。 - 作用域特性:
- 命名空间仅影响编译器对标识符的 “查找逻辑”,不影响变量的生命周期;
- C++ 中的作用域类型包括:函数局部域、全局域、命名空间域、类域。
7.3 命名空间的使用方式
编译器默认仅在局部域和全局域中查找标识符,若需使用命名空间内的成员,需通过以下方式访问,推荐度从高到低依次为:
- 指定命名空间访问(项目开发推荐):通过
命名空间名::成员名的格式精准访问,无命名冲突风险。cpp
运行
int main() { // 访问bit命名空间中的rand变量 printf("%d\n", bit::rand); // 访问bit::pg子命名空间中的Add函数 printf("%d\n", bit::pg::Add(1, 2)); return 0; } using展开单个成员:使用using 命名空间名::成员名将命名空间中的单个成员纳入当前作用域,适合高频使用且无冲突的成员。cpp
运行
// 展开bit::hg命名空间中的rand成员 using bit::hg::rand; int main() { // 直接使用展开的rand成员 printf("%d\n", rand); // 其他成员仍需指定命名空间 printf("%d\n", bit::pg::rand); return 0; }using展开全部成员(仅推荐小练习使用):使用using namespace 命名空间名将命名空间中的所有成员纳入当前作用域,项目开发中易引发命名冲突,不推荐使用。cpp
运行
// 展开bit命名空间的所有成员 using namespace bit; int main() { // 直接使用bit命名空间中的rand成员 printf("%d\n", rand); // 子命名空间成员仍需指定子命名空间 printf("%d\n", pg::Add(1, 2)); return 0; }
8. C++ 输入 & 输出
C++ 通过<iostream>库提供输入输出功能(简称 IO 流),核心优势是 “自动识别变量类型” 和 “支持自定义类型的输入输出”,无需像 C 语言那样手动指定格式符。
8.1 核心组件与功能
| 组件 | 功能说明 | 用法示例 |
|---|---|---|
std::cin | 标准输入对象,对应键盘输入,>>为流提取运算符,用于读取数据 | int a; double b; cin >> a >> b;(自动识别a和b的类型并读取) |
std::cout | 标准输出对象,对应屏幕输出,<<为流插入运算符,用于输出数据 | cout << a << " " << b << endl; |
std::endl | 特殊操作符,作用是插入换行符并刷新输出缓冲区(区别于\n仅插入换行符) | cout << "hello C++" << endl; |
<iostream> | C++ 标准输入输出流库头文件,必须包含才能使用cin、cout、endl | 代码开头需添加#include <iostream> |
8.2 关键特性与注意事项
- 自动类型识别:编译器通过函数重载实现变量类型的自动匹配,无需像
printf/scanf那样指定格式符(如%d、%lf)。cpp
运行
int a = 0; double b = 0.1; char c = 'x'; // 自动识别类型并输出 cout << a << " " << b << " " << c << endl; // 自动识别类型并读取 cin >> a >> b >> c; - 效率优化:在大量输入的场景(如算法竞赛)中,可通过以下代码关闭 C 与 C++ IO 流的同步,提升输入输出效率:
cpp
运行
#include<iostream> using namespace std; int main() { ios_base::sync_with_stdio(false); // 关闭C和C++ IO同步 cin.tie(nullptr); // 解除cin与cout的绑定 cout.tie(nullptr); // 后续输入输出操作效率提升 return 0; } - 命名空间依赖:
cin、cout、endl均属于 C++ 标准库,封装在std命名空间中,使用时需通过std::前缀或using namespace std展开。
9. 缺省参数
缺省参数(又称默认参数)是指在声明或定义函数时,为函数的形参指定一个默认值。调用函数时,若未传递实参,则使用形参的默认值;若传递了实参,则使用指定的实参。
9.1 缺省参数的分类与规则
| 分类 | 代码示例 | 核心规则 |
|---|---|---|
| 全缺省参数 | ```cpp | |
| void Func1(int a = 10, int b = 20, int c = 30) | ||
| { | ||
| cout << "a = " << a << ", b = " << b << ", c = " << c << endl; | ||
| } | ||
| ``` | 所有形参均指定默认值,调用时可传递 0~n 个实参(如Func1()、Func1(1)、Func1(1,2)、Func1(1,2,3)均合法)。 | |
| 半缺省参数 | ```cpp | |
| void Func2(int a, int b = 10, int c = 20) | ||
| { | ||
| cout << "a = " << a << ", b = " << b << ", c = " << c << endl; | ||
| } | ||
| ``` | 仅部分形参指定默认值,必须从右往左连续缺省,不可间隔缺省(如void Func(int a=10, int b)为非法写法);调用时需从左到右传递实参(如Func2(100)、Func2(100,200)、Func2(100,200,300)均合法)。 |
9.2 注意事项
- 声明与定义分离时,缺省值仅能在声明中指定:若函数声明和定义分离(如头文件
.h中声明,源文件.cpp中定义),缺省参数只能在声明中设置,定义中不可重复设置,避免编译器产生歧义。cpp
运行
// Stack.h(函数声明,指定缺省值) typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps, int n = 4); // Stack.cpp(函数定义,不指定缺省值) #include "Stack.h" void STInit(ST* ps, int n) { assert(ps && n > 0); ps->a = (STDataType*)malloc(n * sizeof(STDataType)); ps->top = 0; ps->capacity = n; } - 实用场景:初始化容器时,可根据需求指定初始大小。例如栈初始化时,默认开辟 4 个空间;若已知需存储 1000 个数据,可直接传递 1000 作为实参,避免后续扩容操作。
10. 函数重载
函数重载是 C++ 的重要特性,允许在同一作用域中定义同名函数,但要求这些同名函数的形参满足 “不同”(形参个数不同、形参类型不同或形参顺序不同),从而实现灵活的函数调用。C 语言不支持函数重载。
10.1 函数重载的判定条件
| 重载类型 | 代码示例 | 说明 |
|---|---|---|
| 形参类型不同 | ```cpp | |
| //int 类型参数的 Add 函数 | ||
| int Add(int left, int right) | ||
| { | ||
| cout << "int Add(int, int)" << endl; | ||
| return left + right; | ||
| } | ||
| //double 类型参数的 Add 函数 | ||
| double Add(double left, double right) | ||
| { | ||
| cout << "double Add(double, double)" << endl; | ||
| return left + right; | ||
| } | ||
| ``` | 调用时,编译器根据实参类型匹配对应的函数(如Add(1,2)调用int类型版本,Add(1.1,2.2)调用double类型版本)。 | |
| 形参个数不同 | ```cpp | |
| // 无参版本的 f 函数 | ||
| void f() | ||
| { | ||
| cout << "f()" << endl; | ||
| } | ||
| // 一个 int 参数版本的 f 函数 | ||
| void f(int a) | ||
| { | ||
| cout << "f(int)" << endl; | ||
| } | ||
| ``` | 调用时,编译器根据实参个数匹配对应的函数(如f()调用无参版本,f(10)调用int参数版本)。 | |
| 形参顺序不同 | ```cpp | |
| // 先 int 后 char 的 f 函数 | ||
| void f(int a, char b) | ||
| { | ||
| cout << "f(int, char)" << endl; | ||
| } | ||
| // 先 char 后 int 的 f 函数 | ||
| void f(char b, int a) | ||
| { | ||
| cout << "f(char, int)" << endl; | ||
| } | ||
| ``` | 调用时,编译器根据实参顺序匹配对应的函数(如f(10, 'a')调用第一个函数,f('a', 10)调用第二个函数)。 |
10.2 不构成重载的情况
- 返回值不同不能作为重载条件:函数调用时,编译器无法根据返回值类型区分同名函数(如
int f()和void f(),调用f()会报错)。 - 缺省参数可能导致歧义:若同名函数的参数个数相同且包含缺省参数,调用时可能出现匹配歧义。例如
void f1()和void f1(int a=10),调用f1()时,编译器无法确定使用哪个函数,会报歧义错误。
11. 引用
引用是 C++ 中的重要概念,本质是给已存在的变量取一个 “别名”,引用与原变量共用同一块内存空间,编译器不会为引用开辟额外的内存。引用的核心用途是优化函数传参和返回值,减少数据拷贝。
11.1 引用的概念与定义
- 语法格式:
类型& 引用名 = 原变量(此处&为引用符号,非取地址符号,需与指针的&区分); - 代码示例:
cpp
运行
#include<iostream> using namespace std; int main() { int a = 0; int& b = a; // b是a的别名 int& c = a; // c也是a的别名 int& d = b; // d是b的别名,本质仍是a的别名 ++d; // 修改d等同于修改a,a的值变为1 // 引用与原变量地址相同 cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }
11.2 引用的三大特性
- 定义时必须初始化:引用是变量的别名,必须在定义时明确绑定一个已存在的变量,未初始化的引用会导致编译报错(如
int& ra;为非法写法)。 - 一个变量可以有多个引用:如同一个人可以有多个名字,一个变量也可以有多个别名(如上述代码中
a的别名b、c、d)。 - 引用一旦绑定实体,不可更改指向:引用绑定某个变量后,始终指向该变量,无法重新指向其他变量。例如
b = c的操作是将c的值赋给b(即赋给a),而非让b重新指向c。
11.3 引用的实际用途
引用的核心价值在于 “减少数据拷贝” 和 “直接操作原变量”,主要应用于函数传参和函数返回值场景。
11.3.1 引用传参
引用传参可替代指针传参,简化代码逻辑,同时避免指针的复杂性(如二级指针),且能直接修改原变量的值,功能与指针传参一致。
- 代码示例:实现交换函数
cpp
运行
#include<iostream> using namespace std; // rx是x的别名,ry是y的别名 void Swap(int& rx, int& ry) { int tmp = rx; rx = ry; ry = tmp; } int main() { int x = 0, y = 1; cout << "交换前:x = " << x << ", y = " << y << endl; Swap(x, y); // 直接传递变量,无需传递地址 cout << "交换后:x = " << x << ", y = " << y << endl; return 0; } - 实用场景:在数据结构操作中(如链表插入、栈初始化),使用引用传参可避免二级指针。例如链表头插时,使用
ListNode*& phead(指针的引用),直接修改头指针,无需传递二级指针。
11.3.2 引用返回值
当函数返回的是 “函数结束后不会销毁的变量”(如全局变量、类成员变量、动态内存分配的变量)时,使用引用返回可避免返回值的拷贝操作,提升程序效率。若返回局部变量,引用会成为 “野引用”(局部变量销毁后,引用指向的内存空间失效),导致程序错误。
- 代码示例:获取栈顶元素(栈顶元素存储在栈结构中,函数结束后不销毁)
cpp
运行
#include<iostream> #include<assert.h> using namespace std; typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST& rs, int n = 4) { rs.a = (STDataType*)malloc(n * sizeof(STDataType)); rs.top = 0; rs.capacity = n; } void STPush(ST& rs, STDataType x) { assert(rs.top <= rs.capacity); // 扩容逻辑(此处省略) rs.a[rs.top] = x; rs.top++; } // 引用返回栈顶元素 int& STTop(ST& rs) { assert(rs.top > 0); // 确保栈非空 return rs.a[rs.top - 1]; // 返回栈顶元素的引用 } int main() { ST st; STInit(st); STPush(st, 2); STTop(st) += 10; // 直接修改栈顶元素(2变为12) cout << "栈顶元素:" << STTop(st) << endl; // 输出12 return 0; }
