C++基础
- [1. 命名空间(namespace)](#1. 命名空间(namespace))
-
- [1.1 命名空间的价值](#1.1 命名空间的价值)
- [1.2 namespace的定义](#1.2 namespace的定义)
- [1.3 命名空间的使用](#1.3 命名空间的使用)
- [2. C++输入&输出](#2. C++输入&输出)
-
- [2.1 附加:提高C++IO效率](#2.1 附加:提高C++IO效率)
- [3. 缺省参数(默认参数)](#3. 缺省参数(默认参数))
-
- [3.1 全缺省参数和半缺省参数](#3.1 全缺省参数和半缺省参数)
- [3.2 缺省参数的使用](#3.2 缺省参数的使用)
- [4. 函数重载](#4. 函数重载)
-
- [1. 参数类型不同](#1. 参数类型不同)
- [2. 参数个数不同](#2. 参数个数不同)
- [3. 参数类型顺序不同](#3. 参数类型顺序不同)
- [5. 引用](#5. 引用)
-
- [5.1 引用的概念和定义](#5.1 引用的概念和定义)
- [5.2 引用的特性](#5.2 引用的特性)
- [5.3 引用的使用](#5.3 引用的使用)
- [5.4 引用做返回值](#5.4 引用做返回值)
- [5.5 const引用](#5.5 const引用)
-
- [5.5.1 const常量 vs const引用](#5.5.1 const常量 vs const引用)
- [5.5.2 普通变量 vs const引用](#5.5.2 普通变量 vs const引用)
- [5.5.3 常数 vs const引用](#5.5.3 常数 vs const引用)
- [5.5.4 表达式 vs const引用](#5.5.4 表达式 vs const引用)
- [5.5.5 类型转换 vs const引用](#5.5.5 类型转换 vs const引用)
- [5.5.6 疑问](#5.5.6 疑问)
- [5.5.7 实践意义](#5.5.7 实践意义)
- [5.6 指针和引用的关系(面试常考)](#5.6 指针和引用的关系(面试常考))
- [6. inline](#6. inline)
- [7. nullptr(C++11)](#7. nullptr(C++11))
1. 命名空间(namespace)
1.1 命名空间的价值
在C/C++中,变量、函数和后面学到的类都是大量存在的,整型变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
使用命名空间的目的,就是对标识符的名称进行本地化,避免命名冲突。
C语言程序,可能会出现以下问题:
c
#include<stdio.h>
#include<stdlib.h>
int rand=10;
int main()
{
//编译报错:"rand": 重定义;以前的定义是"函数"
printf("%d\n",rand);
return 0;
}
C++引入namespace,就是来解决上述C语言出现的命名冲突问题的。
1.2 namespace的定义
-
定义命名空间,需要用到
namespace关键字,后面跟命名空间的名字,然后接一对{}即可(注,末尾无需加分号";"){}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
-
namespace本质是定义一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以rand就不再冲突了。 -
C++中域有局部域 ,全局域 ,命名空间域 ,类域。
域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所以有了域隔离,名字冲突就解决了。
局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
-
编译器默认只在局部和全局中找变量/函数/类型,不会去到命名空间找,因此需要指定。
-
namespace只能定义在全局,当然,它还可以嵌套定义。 -
项目工程多文件中,定义的同名
namespace会认为是一个namespace,不会冲突。 -
C++标准库都放在一个叫
std(standard)的命名空间中
1.3 命名空间的使用
- 编译默认规则
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以直接访问命名空间的成员,程序会编译报错。 - ❗️命名空间的使用(重点)
使用命名空间中定义的变量/函数,有三种方式:
- 指定命名空间访问。
✅️项目最推荐 using将命名空间中某个成员展开。
✅️项目常用,成员无冲突时推荐using展开命名空间中的全部成员
❌️项目不推荐(易冲突)
✅️日常练习程序
- 注意事项
- 在访问命名空间中的东西时,需要用到 :: (域作用限定符)
- 使用
using后,可以直接访问对应的成员
举个简单的例子演示:
(注:后续将命名空间的名字简称为"名字",如文中的fuyo)
cpp
//命名空间
namespace fuyo
{
int a = 10;
int b = 20;
}
错误案例❌️
cpp
//直接编译,编译器无法主动查找命名空间里的内容,程序报错
int main()
{
printf("%d\n",a);
return 0;
}
正确使用方法✅️
- 指定命名空间访问
形式 - 名字::成员名
cpp
//指定命名空间访问
int main()
{
printf("%d\n", fuyo::a);
return 0;
}
using将命名空间中的某个成员展开
形式 -using名字 :: 成员名;
cpp
//using 将命名空间中的某个成员展开
using fuyo::b;
int main()
{
printf("%d\n", fuyo::a);
printf("%d\n", b);//可直接访问
return 0;
}
using展开命名空间中的全部成员(日常练习时使用)
形式 -using namespace名字;
cpp
//using 展开命名空间中的全部成员
using namespace fuyo;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
注:
展开头文件和展开命名空间中的成员中的"展开",并不是一个意思。
展开头文件:在预处理的过程中,将头文件中的内容拷贝到当前文件中,也就是"纯文本替换"。
2. C++输入&输出
-
<iostream>是Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。可以将其类比为C语言的<stdio.h> -
std::cin是istream类的对象,它主要面向窄字符(narrow character(of type char))的标准输入流。 -
std::cout是ostream类的对象,它主要面向窄字符的标准输出流。 -
std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。 -
<<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
-
C++的输入输出可以自动识别变量类型。
-
一般日常练习可以使用
using namespace std,实际项目开发中不建议这样做
补充:
<< 与 >>的用法:
cpp
cout << x; // 把 x 插入到输出流 - 输出 x
cin >> x; // 从输入流提取数据 - 数据放入 x
记忆方法:
<<指向输出流 - 输出 变量数据
>>指向变量 - 数据放入变量
实践
要么指定命名空间,如下:
cpp
#include<iostream>
int main()
{
int i = 10;
std::cout << i << std::endl;//指定命名空间
return 0;
}
要么使用using 展开std,如下:
cpp
#include<iostream>
using namespace std;//使用using展开
int main()
{
int i = 10;
cout << i << endl;
return 0;
}
C++自动识别的变量类型的体现:
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
//可以自动识别变量类型
cin >> a >> b >> c;//流提取 - 可以一次性识别一个或多个
cout << a << endl;//流插入
cout << b << " " << c << endl;
return 0;
}
2.1 附加:提高C++IO效率
cin和cout,为了兼容C语言,缓冲区绑定等多种原因,与scanf和printf相比起来,效率较低。
因此 在io需求比较高的地方,如部分大量输入的竞赛题中,加入以下3行代码,可以提高C++IO效率。
cpp
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
3. 缺省参数(默认参数)
-
缺省函数:声明或定义函数时,为函数的参数指定一个缺省值。
-
在调用该函数时,会出现以下情况:
- 没有指定实参时,使用函数中指定的缺省值(默认值)
- 指定实参时,就使用指定的实参。
-
⭐带缺省函数的函数调用,C++规定必须从左往右依次给实参,不能跳跃给实参。
-
⭐函数声明和定义分离时,缺省函数不能在函数声明和定义中同时出现,规定是函数声明给缺省值。
- 函数声明(头文件):写缺省值
- 函数定义(源文件):不写缺省值
代码如下:
cpp
#include<iostream>
using namespace std;
void Func(int a = 0)//默认为0
{
cout << a << endl;
}
int main()
{
Func();//没有传参时,使用参数的默认值(缺省值) - 输出 0
Func(10);//传参时,使用指定的实参 - 输出 10
return 0;
}
3.1 全缺省参数和半缺省参数
- ⭐缺省参数分为全缺省参数 和半缺省参数 。
- 全缺省:全部形参给缺省值。
- 半缺省:部分形参给缺省值,C++规定,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
代码如下:
cpp
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
全缺省的使用示例如下:
cpp
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
半缺省的使用如下:
cpp
//半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
注:使用半缺省参数时,不可以什么都不传!程序会报错!
3.2 缺省参数的使用
代码如下:
cpp
#include<iostream>
using namespace std;
#include"stack.hpp"
int main()
{
fuyo::ST s1;
fuyo::STInit(&s1);
//确定知道要插入1000个数据,初始化时一把开好,避免扩容
fuyo::ST s2;
fuyo::STInit(&s2, 1000);
return 0;
}
4. 函数重载
函数重载:C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,以下三个条件,满足其一即可。
- 参数类型不同
- 参数个数不同
- 参数类型顺序不同
这样C++函数调用就表现出了多态行为,使用更灵活。
注:返回值不同不算重载!
1. 参数类型不同
cpp
#include<iostream>
using namespace std;
//1. 参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(int left, double right)" << endl;
return left + right;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
return 0;
}
2. 参数个数不同
cpp
//2. 参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
3. 参数类型顺序不同
cpp
//3. 参数类型顺序不同
void f(int a, char b)
{
cout << " f(int a, char b)" << endl;
}
void f(char b, int a)
{
cout << " f(char b, int a)" << endl;
}
5. 引用
5.1 引用的概念和定义
引用 不是定义一个新变量,而是给已存在变量取了个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
⭐省流:**引用的概念其实就是给变量取别名,不会额外开辟空间 **。
引用的符号:&(虽然和取地址的符号一样,但是&放到类型后则表示引用)
使用方法:
类型& 引用别名 = 引用对象;
代码示例:
cpp
int main()
{
int a = 0;
//引用:b和c是a的别名
int& b = a;
int& c = a;
//也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;//a,b,c,d都会增加
//这里取地址,可以观察到四者的地址一致
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
5.2 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用(别名)
- 引用一旦引用一个实体,再不能引用其他实体,也就是说,引用是不可以改变指向的。
c
int a = 10;
//编译报错:"ra":必须初始化引用
//int& ra;
int& b = a;
int& c = b;
int d = 20;
b = d;//这里为赋值操作,将d赋值给了b,连同a和c也被赋值了
5.3 引用的使用
- 引用在实践中的功能:
- 在 函数传参和返回值 中减少拷贝效率;
- 改变引用对象的同时,改变实体。
例如,对于两个数据的交换
在C语言中,形参要改变形参,需要对实参进行取地址,代码如下:
c
//C语言中需要对实参进行取地址
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int x = 1, y = 0;
Swap(&x, &y);
return 0;
}
但在C++中,我们可以使用引用来进行数据的交换。
cpp
void Swap(int& rx, int& ry)
{
//rx是x的别名,ry是y的别名,别名改变会导致实体改变
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 1, y = 0;
Swap(x, y);
return 0;
}
指针变量也可以取别名,这样就不用传二级指针了,相对而言简化了程序。
示例如下:
cpp
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
// LTNode = struct ListNode
// PNode = struct ListNode*
// 这⾥LTNode*& phead就是给指针变量取别名
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
PNode* plist = NULL;
ListPushBack(plist, 1);
return 0;
}
5.4 引用做返回值
想要弄懂引用做返回值,首先要搞清楚函数返回值的原理:
函数不能直接将内部变量返回给调用者,而是先生成一个临时对象 ,将内部变量的值拷贝给这个临时对象后,再返回。
⭐临时对象 具有常性,因此调用者不可修改。
引用做返回值,则是返回这个内部变量的别名,调用者可以进行修改。
✅️正确使用案例 :直接修改栈顶元素(如图所示)

❗️注意:
引用做返回值不可以取代普通的返回。
引用做返回值,是有条件的。
- 引用必须绑定在一个,出函数后,不会被销毁的对象。
❌️错误案例:
cpp
int& func()
{
int a = 0;
return a;//返回局部变量引用,错误!
}
也就是说,出函数后就被销毁的局部变量 ,不可以使用引用返回。
⭐不能使用局部变量做引用返回值的原因:
局部变量出函数后就销毁,内存被回收,而它的引用,则指向一块废弃的内存空间 ,此时的引用 类似于野指针,会出现运行问题。
5.5 const引用
⭐引用的核心规则:权限只能缩小,不能放大。
- 权限缩小:普通变量 → const引用
可读可写 → 只读
- 权限不可扩大:const常量、常数、表达式、类型转换 → const引用
只读 → 只读
5.5.1 const常量 vs const引用
- const常量也可以被引用,但是必须使用const引用!
❌️错误案例
cpp
const int a = 10;
int& ra = a;
这是因为,引用的时候,权限可以缩小,但不能放大。
被const限制的常量a,权限为只读不可写,若直接引用,相当于扩大它权限,使其变为可读可写,而这绝对是违规的。
因此我们需要使用一个,同样被const限制的引用ra,来给它取别名(如下)
✅️正确示范
cpp
const int a = 10;
const int& ra = a;
5.5.2 普通变量 vs const引用
- 我们可以使用const引用,来给变量取别名,而这就体现了引用权限可以缩小的特点。
示例如下:
cpp
//引用权限的缩小
int b = 10;//b是变量可以被修改,b的变化也会造成rb的变化
const int& rb = b;//rb被const限制,不可以被修改
5.5.3 常数 vs const引用
- const引用的对象可以是常数(注,普通引用不可以引用常数)
cpp
int& rc =30;//❌️错误案例
const int& rd = 30;//✅️正确示范
5.5.4 表达式 vs const引用
- 注意,表达式的返回值同样存储在临时对象中,临时对象具有常性。
临时对象:
编译器需要一个空间暂存表达式的求值结果 时,临时创建的一个未命名的对象。
C++中,把这个未命名对象叫做临时对象。
- 因此,若想给表达式的返回值取别名,也需要使用const引用。
cpp
int a = 5;
int b = 10;
int& rc = a + b;//❌️错误案例
const int& rd = a + b;//✅️正确示范
//a+b的是一个表达式,其求值结果具有常性,需要const引用!
5.5.5 类型转换 vs const引用
- 使用不同于本体的数据类型,进行赋值/引用 时,会发生类型转换,也会产生一个具有常性的临时对象。
- 所以,需要使用 const引用 给其取别名。
cpp
double a = 3.14;
int b = a;
int& rc = a;//❌️错误案例
const int& rd = a;//✅️正确示范
//类型转换会产生临时对象,需要使用const引用
5.5.6 疑问
❓ask:用const引用,给临时对象取别名,临时对象一销毁,const引用不就出问题了吗?
⭕答:
不会。当const引用临时对象时,临时对象的生命周期就会和const引用绑定,const引用销毁,临时对象才会被销毁。
5.5.7 实践意义
const引用常被用于函数传参中(不需要利用形参改变实参时)。
原因如下:
- const引用可以扩大传参范围,它可以接收普通变量、const常量、表达式、类型转换。
- 引用可以减少内存拷贝。
- 保证数据安全,函数内部不会改变对象。
5.6 指针和引用的关系(面试常考)
C++中指针和引用像两个性格迥异的亲兄弟,在实践运用中,二者相辅相成,功能有重叠性,但是各有自己的特点,互相不可取代。
- 语法概念上,引用是一个变量的取别名,不开空间;指针是存储一个变量的地址,要开空间。
- 引用在定义时,必须初始化;指针建议初始化。
- 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象。
- sizeof中含义不同。引用结果为引用类型的大小;而指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对安全一点(ps:不是绝对安全)
6. inline
-
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。
-
inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。
-
inline适用于被频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
-
C语言实现宏定义函数时,也会在预处理阶段替换展开,但是,宏函数实现复杂又容易出错,且不方便调试,C++设计inline目的就是替代C的宏函数。
-
vs编译器debug版本下,默认不展开inline,这样方便调试。debug版本想展开需要设置以下两个地方:
- 打开"属性",找到C/C++ 底下的"常规",将"调试信息格式"改为"程序数据库"
- 在C/C++ 底下的"优化"中,将内联函数扩展,改为"inline"
inline不展开 = 虽然写了inline,但编译器依然把它当普通函数调用。
inline展开 = 将函数体"复制粘贴"到调用处,不执行函数调用的过程。
展开的优缺点:
-
优点:
- 消除函数调用开销,提升程序运行速度。适用于简短函数。
-
缺点:
- 不可用于递归和复杂的大函数,内联的大函数展开,会导致可执行程序体积暴增。
例如:100条指令,有10000个位置调用
不展开的指令数:10000+100条
展开的指令数:10000*100条(可执行程序体积暴增!)
- inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开时,没有函数地址,链接时会出现报错。
❌️错误案例:
cpp
//F.h
#include<iostream>
using namespace std;
inline void f(int i);
#include"F.h"
void f(int i)
{
cout << i << endl;
}
//main.cpp
#include"F.h"
int main()
{
f(10);//链接错误:无法解析的外部符号
return 0;
}
原因:
声明和定义分离到两个文件时,main.cpp只能找到内联函数的声明,函数体在F.cpp中,不仅无法链接,也会丧失内联性质
✅️正确示范:内联直接放到.h文件里
cpp
//F.h
#include<iostream>
using namespace std;
inline void f(int i)
{
cout << i << endl;
}
//main.cpp
int main()
{
f(10);
return 0;
}
7. nullptr(C++11)
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到以下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在C语言中,允许void*的指针隐式类型转换成其他类型的指针。
但C++十分严格,void*的指针必须进行强制类型转换后才能转换。
✅️C语言:允许void*指针隐式类型转换,不会报错
c
//C语言
void* pa = NULL;
int* pb = pa;//不会报错
❌️C++:禁止隐式类型转换(必须使用强制类型转换)
cpp
//C++
void* pa = NULL;
int* pb = pa;
//无法将void* 直接转换成int*
✅️C++:允许强制类型转换
cpp
//C++
void* pa = NULL;
int* pb = (int*)pa;
- C++11中引入
nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。 - 使用
nullptr定义空指针可以避免类型转换的问题,因为nullptr能被隐式类型转换 为指针类型。(注:nullptr不能被转换为整型)

