☰
深挖C++:C++入门基础
2026/11/3 10:47:48 网站建设 项目流程

目录

绪论:C++版本更新

一、C++的第一个程序

二、命名空间

2.1 namespace

2.2 作用域操作符 ::

2.3 命名空间的使用

三、C++输入&输出

四、缺省参数

五、函数重载

六、引用

6.1 定义

6.2 特性

6.3 使用

6.4 const引用

6.5 指针和引用的关系

七、inline函数

八、nullptr


此处省略C++的发展历史……

绪论:C++版本更新

C++从多年一更变成了固定的三年一更,中间经历了许多的插曲,最后思路转变成从 "等所有东西都完美了再发布" 变成 "到点就发,好的坏的都按班车走"。这就是现在能看C++11/14/17/20/23/26 这样整齐的 3 年节奏。

C++参考文档:

1.https://legacy.cplusplus.com/reference/

— 非官方,只到 C++11,但按头文件组织、好懂,适合入门查用法

2.https://zh.cppreference.com/w/cpp

— 官方中文版,信息全、更新到最新标准,但偏硬核

3.https://en.cppreference.com/w/

— 官方英文版,最权威,内容同中文版

总结:入门查用法看第一个,查最新标准细节看后两个,结合着用。


一、C++的第一个程序

C++兼容C语言绝大多数的语法,所以C语言实现的Hello World依旧可以运行,C++需要把定义的文件后缀改为 .cpp ,vs编译器看到 .cpp 就会调用C++编译器编译。其他平台也有相关的C++编译器,比如Linux下是g++。

// C++代码 #include<iostream> using namespace std; int main() { cout << "Hello World!" << endl; return 0; }

二、命名空间

2.1 namespace

本质是圈出一个独立作用域,解决同名冲突。

语法:

namespace 名字 { 变量/函数/类型 }

规则:

1、C++有四种域:函数局部域、全局域、命名空间域、类域,都用于编译时名字查找。

2、局部域/全局域会影响变量声明周期;命名空间域/类域不影响的生命周期。

3、namespace只能写在全局,可以嵌套;多文件里同名namespace自动合并,不冲突。

4、C++标准库全部放在std命名空间里。

总结:namespace 就是给变量 / 函数套个 "家族姓",避免不同模块撞名;只管编译时叫谁,不管变量活多久。

以下C语言代码会报错:

#include<stdio.h> #include<stdlib.h> int main() { int rand = 9; printf("%d", rand); return 0; }

报错如下,“找到一个或多个重定义的符号”,主要原因是stdlib标准库中已经有一个名为 rand 的函数,此时在C语言这里再次定义一个整型变量命名为rand就会报错,需要C++中namespace来解决

C++解决代码如下:

#include<stdio.h> #include<stdlib.h> namespace xys { int rand = 9; // 函数 int add(int a, int b) { return a + b; } // 类型 struct Stu { char name; int age; }; } int main() { printf("%d", xys::rand); return 0; }

利用namespace来声明了一个域,把整型变量rand放在改域里就不会报错了,同时也可以在域中可以定义变量、函数、类型。

细心的博友们已经在这里发现了我在问namespace中的rand时,加上了前缀 xys:: ,那么接下来我来详细讲解为什么加上此前缀,以及新的操作符。

2.2 作用域操作符 ::

作用:明确指定标识符所属的作用域,解决命名冲突、访问被内层作用域屏蔽的外层实体。

1. 访问全局作用域

当局部变量与全局变量命名冲突时,局部变量会屏蔽全局变量;在变量名前加 :: 即课显示访问全局作用域的实体。

int num = 100; // 全局变量 int main() { int num = 10; // 局部变量,屏蔽了全局num cout << num; // 输出 10(局部变量) cout << ::num; // 输出 100(全局变量) }

2. 访问命名空间成员

std::cout << "hello"; // 访问 std 命名空间中的 cout std::string str; // 访问 std 命名空间中的 string

嵌套命名空间可逐层使用 :: 访问:

namespace A { namespace B { int val = 5; } } int x = A::B::val; // 访问嵌套命名空间的成员

3. 访问类的成员

在类外部定义成员函数时,必须用类名::函数名指明该函数的所属类。

访问类的静态成员(静态变量/静态函数)时,直接通过类名 ::访问,无需实例化对象。

class Student { public: static int count; // 静态成员声明 void show(); // 成员函数声明 }; // 类外定义成员函数 void Student::show() { /* ... */ } // 类外初始化静态成员 int Student::count = 0; int main() { int n = Student::count; // 直接通过类名访问静态成员 }

2.3 命名空间的使用

编译器默认仅在局部和全局作用域查找标识符,不会自动检索命名空间,直接访问命名空间内的成员会编译报错。使用命名空间成员有三种方式:

1、指定命名空间访问(加::前缀):项目开发推荐使用

namespace N { int apple = 3; int banana = 5; } int main{ std::cout << N::apple << std::endl; return 0; }

通过命名空间名 :: 成员名明确指定作用域,无命名冲突风险,我们常用的std::out就属于这种方式。

2、using 展开单个成员:适合频繁访问且无命名冲突的成员

namespace N { int apple = 3; int banana = 5; } using N::apple; // 只展开命名空间N中的成员apple int main{ std::cout << apple << std::endl; // apple 前不用再加上 N:: return 0; }

仅展开指定的成员,该成员可直接使用;未展开的成员仍需加命名空间前缀,适合频繁访问且无冲突的成员。

3、展开全部命名空间成员:仅适合小型练习代码,项目中冲突风险高,不推荐

namespace N { int apple = 3; int banana = 5; } using namespace N; // 展开N命名空间的所有成员 int main{ std::cout << apple << std::endl; // apple 前不用加上 N:: std::cout << banana << std::endl; // banana 前不用加上 N:: return 0; }

using namespace 命名空间名会把该命名空间所有成员全部引入当前作用域,写法简便但易引发命名冲突,大型项目不推荐。

我们日常练习代码里常见using namespace std;就是第三种用法,可以省略std::前缀直接写cout、string,但正式项目中一般不这么写。


三、C++输入&输出

1、<iostream>是标准输入输出流库,提供std::cin(标准输入流对象)、std::out(标准输出流对象);std::endl是操纵符,作用为换行并刷新输出缓冲区。

2、通过<<(流插入 / 输出)、>>(流提取 / 输入)运算符完成 IO 操作,依靠函数重载自动识别变量类型,无需手动指定格式,同时支持自定义类型的输入输出。

3、所有 IO 组件都归属于std命名空间;练习代码可用using namespace std;简化写法,正式项目不推荐。

4、VS 编译器下引入<iostream>可间接使用printf/scanf,但不具备通用性;大数据量输入场景(如算法竞赛),可通过关闭 stdio 同步、解绑 cin 与 cout 来提升 IO 效率。

#include <iostream> using namespace std; // 展开std命名空间,仅推荐练习使用 int main() { double point = 0.0; cin >> point; cout << point << endl; int juice = 0; cin >> juice; cout << juice << endl; return 0; }

四、缺省参数

定义:函数声明时给参数一个默认值,调用时不传就自动用这个默认值传进去。

代码示例:

#include <iostream> // b和c都有默认值 int sum(int a, int b = 0, int c = 0) { return a + b + c; } using namespace std; int main() { cout << sum(1); // 1,相当于sum(1, 0, 0) cout << sum(1, 2); // 3,相当于sum(1, 2, 0) cout << sum(1, 2, 3); // 6,全自己传 }

全缺省:所有参数都有默认值

半缺省:只有部分参数有默认值

三条硬规则

1.半缺省必须从右往左连续给默认值,不能跳着给

void f(int a, int b = 10, int c); // 错误!b有默认值但c没有,c在b右边 void f(int a, int b, int c = 10); // 正确 void f(int a, int b = 10, int c = 20); // 正确

2.声明和定义分离时,默认值只写在声明里

// 头文件里: void f(int a = 10); // .cpp里: void f(int a) { ... } // 不要再写=10,否则重复定义

3.不能和函数重载产生歧义

void f(int a = 10) { } void f() { } // 错误!调用f()时编译器不知道调哪个: // 是调无参版本,还是调f(10)?

底层原理:缺省参数不是运行时填充的,是编译期在调用点直接把默认值塞进去:

sum(1); // 编译后等价于: sum(1, 0, 0);

五、函数重载

定义:同一个作用域可以定义同名函数,但参数列表必须不同(C语言不支持,C++支持)

三种合法重载形式:

1、参数类型不同:

Add(int,int); Add(double,double);

2、参数个数不同:

Add(int,int); Add(int,int,int);

3、参数类型顺序不同:

f(int,char); f(char,int);

不能算重载的情况

1、返回值不同:调用时编译器只看实参,看不到返回值,无法区分。

2、和缺省参数产生歧义:f() 和 f(int a = 10) 同时存在时,调 f() 编译器不知道该选哪个,产生歧义。


六、引用

6.1 定义

本质:不是新定义变量,而是给已存在的变量取个别名,编译器不为引用分配新内存,它和原变量共用同一块内存空间。

语法:

类型& 别名 = 原变量;

代码示例:

#include<iostream> using namespace std; int main() { int a = 10; int& b = a; int& c = b; int& d = c; cout << a << endl; cout << b << endl; cout << c << endl; cout << d << endl; cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }

运行结果:

6.2 特性

三个规则:

1、引用在定义时必须初始化。

2、一个变量可以有多个引用。

3、引用一旦绑定就不能改绑。

6.3 使用

引用的使用主要在两部分:引用传参和引用返回

引用传参:

类似指针传参,功能相同但时写法更简洁——函数改形参就是改实参,还省去了拷贝。

代码示例:

void Swap(int& rx, int& ry) { int tmp = rx; rx = ry; ry = tmp; } int main() { int x = 0, y = 9; Swap(x, y); // 直接传变量,不用传地址 // x变成1,y变成0 cout << x << " " << y << endl; Swap(x, y); cout << x << " " << y << endl; }

传引用返回:

代替传值返回,避免产生临时对象,还能让返回值当左值使用。

代码示例:

// 传值返回:返回临时对象,不能做左值 // int STTop(ST& rs) { // return rs.a[rs.top]; // } // STTop(st1) += 10; // 编译错误:+=左边必须是左值 // 传引用返回:返回栈顶元素的别名 int& STTop(ST& rs) { assert(rs.top > 0); return rs.a[rs.top]; } int main() { ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); STTop(st1) += 10; // 直接把栈顶元素2改成12 cout << STTop(st1); // 输出12 }

小补充:

左值 vs 右值

一个简单的判断方法

一个表达式,能不能取地址(&)?

能取地址 →左值

不能取地址 →右值

左值(lvalue)右值(rvalue)
本质有名字、有持久内存的东西临时的、用完即毁的东西
能不能取地址能(&x合法)不能(&5编译错误)
能不能放=左边能不能
能不能放=右边能能
生命周期一直存在,直到离开作用域语句结束就销毁

代替二级指针——链表头插

typedef struct ListNode { int val; struct ListNode* next; } LTNode, *PNode; // 用 PNode& phead 代替 LTNode** phead,写法更直观 void ListPushBack(PNode& phead, int x) { PNode newnode = (PNode)malloc(sizeof(LTNode)); newnode->val = x; newnode->next = NULL; if (phead == NULL) { phead = newnode; // 直接改phead,不用*phead = newnode } } int main() { PNode plist = NULL; ListPushBack(plist, 1); // 直接传plist,不用&plist }

总结:引用传参就是给实参起个别名进函数用,传引用返回就是给函数内部某个变量起别名带出去——少写指针、少拷贝、还能直接改。

6.4 const引用

const引用规则:权限只能缩小,不能放大。

例如:

普通变量(读写权限)→ const 引用(只读权限):可以(缩小权限)

const 变量(只读权限)→ 普通引用(想读写):不可以(放大权限)

临时对象必须用 const 引用绑定

像 a*3 的结果、double 转 int 产生的中间值,编译器会生成一个临时对象暂存,这个临时对象自带常性,普通引用绑不上,必须用const + &

int main() { // 1. const对象必须用const引用 const int a = 10; // int& ra = a; // ✗ 权限放大:a只读,你想通过ra改它 const int& ra = a; // ✓ 正确 // 2. 普通对象可以用const引用(权限缩小) int b = 20; const int& rb = b; // ✓ rb只读,但b本身还能改 // rb++; // ✗ rb是const,不能通过rb改 // 3. 临时对象必须用const引用 // int& rc = a * 3; // ✗ a*3是临时对象,常性,普通引用绑不上 const int& rc = a * 3; // ✓ // 4. 类型转换也产生临时对象 double d = 12.34; // int& rd = d; // ✗ double转int产生临时对象 const int& rd = d; // ✓ }

6.5 指针和引用的关系

对比项引用指针
本质变量别名,不开新内存存储地址的变量,要开空间
初始化定义时必须初始化建议初始化,语法上不强制
改指向绑死一个对象后不能再换可以随意改指向
访问方式直接用,不用解引用要 * 解引用才访问对象
sizeof结果是引用类型本身的大小永远是地址大小(32 位 4 字节,64 位 8 字节)
安全性很少出问题,相对安全容易出现空指针、野指针

总结:引用是更安全、更简洁的 "别名",但不能改绑、不能空;指针更灵活但要自己管地址、容易出错。实践中该用哪个用哪个,不能互相替代。


七、inline函数

为什么要有inline函数?

C 语言的宏函数在预处理时直接文本替换,复杂宏容易出错、不好调试;C++ 设计 inline 就是为了替代宏函数,解决宏的问题。

inline函数能做什么?

建议编译器在调用点把函数体直接展开,省去函数调用的压栈 / 跳转 / 返回开销(和宏效果类似,但它是真正的函数,有类型检查、作用域)

注意:

1、inline 只是给编译器的建议,不是强制;函数太复杂(递归、循环太多)编译器不会展开。

2、vs 调试:debug 版本默认不展开 inline(方便打断点调试),release 版本才展开;想在 debug 里展开要改项目设置。

3、关键规则:inline 的声明和定义必须放同一个文件(一般直接写在头文件里),不能分离到.h 和.cpp,否则链接错误。

#include <iostream> using namespace std; // 直接写在头文件/同一个文件里 inline int Add(int x, int y) { return x + y; } int main() { int ret = Add(8, 9) * 3; cout << ret << endl; }

对比宏函数的坑

// 宏的问题:不加括号会出bug // #define ADD(a, b) a + b // 错误:ADD(1,2)*5 会变成 1+2*5=11 #define ADD(a, b) ((a) + (b)) // 正确:外层括号+内层括号都要加 int main() { cout << ADD(1, 2) * 5 << endl; // ((1)+(2))*5 = 15 }

错误用法:

声明定义分离导致链接错误

// F.h #include <iostream> using namespace std; inline void f(int i); // 只声明 // F.cpp #include "F.h" void f(int i) { // 定义 cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); // 链接错误!找不到f的地址(inline被展开后没有独立函数地址) }

总结:inline 就是 "带类型检查的宏"—— 写在头文件里,建议编译器把函数体直接塞进调用点,既快又安全;千万别把声明和定义分到两个.cpp 文件里,否则链接报错。


八、nullptr

定义:nullptr是C++11引入的关键字,专门用来表示空指针。它不是0,不是NULL,是一个独立类型 nullptr_t 的字面值。

// 头文件里定义(不用自己写) typedef decltype(nullptr) nullptr_t;

注意:nullptr只能隐式转换成指针类型,不能转成整数。

什么是"隐式转换"?

隐式转换= 编译器自动帮你转,你不用写任何代码。

显式转换= 你自己写强转,比如 (int*)NULL 。

问题:在C++中,NULL就是 0

void f(int x) { cout << "f(int)"; } void f(int* ptr) { cout << "f(int*)"; } f(0); // 调 f(int) f(NULL); // 还是调 f(int)!因为NULL在C++里被定义成0,不是指针 f((int*)NULL); // 手动强转才能调到指针版本 f(nullptr); // ✓ 明确调用 f(int*),不会有歧义

总结:以前用 NULL 表示空指针,但 NULL 在 C++ 里就是 0,函数重载时会误调到 int 版本;C++11 用 nullptr 代替它,专门表示空指针,不会和整数 0 混淆。


最后,感谢你的阅读,欢迎交流与指正!

下一篇我们再见🔜,Bye bye 👋

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询