1.命名空间
- 在C++中回学到有关类的东西,在C的学习中也层学到变量,函数,类的名称都将存在于全局作用域中
- 利用命名空间,可以对标识符的名称进行本地化,避免命名冲突或名字污染
- 一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
1.1 命名空间的定义
①一般的定义
c
//1.一般的命名
namespace N1
{
//1.命名变量
int rand = 10;
//2.函数
int Add(int x, int y)
{
return x + y;
}
//3.结构体
typedef struct Node
{
struct Node* next;
int val;
}Node,*PNode;
}
②嵌套定义
c
//2.嵌套实现
namespace N1
{
//1.命名变量
int rand = 10;
//2.函数
int Add(int x, int y)
{
return x + y;
}
//3.结构体
typedef struct Node
{
struct Node* next;
int val;
}Node, * PNode;
//嵌套结构体访问
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
③创建同名的命名空间
创建同名空间时,编译器会将两个同名空间合并
(一个工程中的test.h和test.cpp的合并成同一个)
c
//1.一般的命名
namespace N1
{
//1.命名变量
int rand = 10;
//2.函数
int Add(int x, int y)
{
return x + y;
}
//3.结构体
typedef struct Node
{
struct Node* next;
int val;
}Node,*PNode;
}
//3. 同名
namespace N1
{
int rand2 = 10;
int rand3 = 100;
}

1.2 命名空间的使用
①加域作用限定符
c
//1.加限定符
cout << N1::rand << endl;
②使用using将特定成员导入
c
using N1::rand;
cout << N1::rand << endl;
③直接导入域名,然后使用
c
using N1::rand;
2.C++的输入和输出
c
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello world"<<endl;
return 0;
}
- cout:标准输出对象(控制台) cin:标准输入对象(键盘),二者必须包含iostream头文件
- 二者都是全局的流对象,endl用于换行
- << 为流插入,>>为流提取
- c++的输入输出可以自动识别变量类型
scanf和cin对空白字符的处理差异
scanf对空白字符的处理取决于字符串的写法:

cin更加智能:
c
int a;
char ch;
cin >> a >> ch;
- cin回自动跳过前导空白字符
- 但是无论输入10\nx还是10 x,ch都会正确读到'x',不会误读回车或空格
- 但是,cin的自动跳过只对>>操作符有效,如果使用cin.get()或getline()仍然会读取到之前输入留下的换行符,需要配合cin.ignore()使用
std命名空间的使用惯例
- 日常联系:直接使用using namespace std即可
- 项目开发:使用时指定命名空间+using std::cout展开常用的库对象/类型等方式
- 日常联系通过这种展开,标准库暴露出来,但是日常的练习时不会怎么出现这种问题,但是在项目开发中就容易出现(毕竟代码多,规模大)冲突问题。
3.缺省参数
- 缺省参数时声明或定义函数时为函数的参数指定一个缺省值
- 调用函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
c
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); //1.不传参,直接使用默认值
Func(10); //2.传参时,使用指定的实参
return 0;
}
3.1 缺省参数分类
①全缺省
c
void Func(int a = 0,int b = 1,int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Func(1);
Func(1, 2);
Func(1, 2, 3);
//Func(, 1, 2); - 参数的赋值需要从左到右,不可以用这种刻意的空白
//Func(1,,2)/Func(1,2,)都不行
return 0;
}
②半缺省
c
void Func(int a,int b,int c=20)
{
...
}
int main()
{
Func(1,2,3);
Func(1,2);//a和b
Func(1);//a
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
c
//违法1
void Func(int a = 10,int c)
{
...
}
//违法2
void Func(int a = 10,int c,int b = 20)
{
...
}
- 缺省参数不能在声明和定义中同时出现
- 声明和定义的位置同时出现,如果两个位置提供的值不同,那编译器就无法确定到底该用哪个缺省值
- 缺省参数是在编译截断确定的,当编译阶段时就必须要知道缺省值是多少才能正确进行函数调用
- 缺省值放在声明才行,因为缺省值只放在定义中,而调用者在另一个文件中,编译器看不到定义,就会报错
c
// func.h
void print(int a, int b = 10); // 缺省值在声明
// func.cpp
#include "func.h"
void print(int a, int b) { // 定义中不写缺省值
cout << a << ", " << b << endl;
}
// main.cpp
#include "func.h"
print(5); // 编译通过,输出 5, 10
- 缺省值必须是常量或者全局/静态变量
c
//局部变量
void test() {
int local = 10;
void func(int a = local); // ❌ 编译错误
}
//非静态成员变量
class MyClass {
int member;
void func(int a = member); // ❌ 编译错误
};
void func() {
static int s_default = 50;
void inner(int a = s_default); // 静态局部变量
} //✅
//this指针相关
class MyClass {
void func(int a = this->x); // ❌ 编译错误
};
4.函数重载
4.1 概念
- 允许在同一作用域声明功能类似的同名函数
- 这些同名函数的形参个数/参数个数/类型/类型顺序不同,用于处理不同的问题
c
//1.参数类型不同
int Add(int left, int right)
{
cout << "int" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double" << endl;
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;//
//cout << Add(1.0, 2) << endl;
cout << Add(1.0, 2.0) << endl;
return 0;
}

c
//2.参数个数不同
double Add(double left, double right)
{
cout << "double" << endl;
return left + right;
}
double Add(double left)
{
cout << "One sigle" << endl;
return left;
}
int main()
{
cout << Add(1.0, 2.0) << endl;
cout << Add(1.0) << endl;
}

c
//3.参数类型顺序不同
int Add(int left, double right)
{
cout << "int" << endl;
return left + right;
}
int Add(double left, int right)
{
cout << "change" << endl;
return left + right;
}
int main()
{
cout << Add(1.0, 2) << endl;
cout << Add(1,2.0) << endl;
}

4.2 函数重载的原理
- C/C++中,一个程序要运行起来,需要经过预处理、编译、汇编和链接这四个阶段

- 在C语言中学习的编译链接可以知道,编译后链接前,a.o文件中没有Add的函数地址,通过链接,连接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起
- 每个编译器都有自己的函数名修饰规则
- C语言编译器:Linux下,gcc编译完成后,函数名不变

- C语言编译器:Linux下,gcc编译完成后,函数名不变
- C++编译器:函数名字修饰改变,编译器将函数参数类型信息添加到修改后的名字中
- Window下的名字修饰规则

- Window下的名字修饰规则
- 所以,根据上述分析可知:C语言没有办法重载,因为同名函数没办法区分,而C++通过函数修饰规则进行区分,参数不同,修饰出来的名字也就不同
5. 引用
5.1 概念
- 说人话就是:取了一个别名
- 从语法的角度来说,别名就是别名,不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
- 类型& 引用变量名 = 引用实体
c
int main()
{
int a = 10;
int& ra = a;
}

5.2 引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,就不能再引用其他实体(不能改变引用的指向)
c
int main()
{
int a = 10;
//int&a; - 不行
int& b = a;
int& c = a;
//int& c = d; - 不能改变引用的指向
}


5.3 常引用
常变量只能通过常引用引用
c
int main()
{
const int a = 10;
const int& ra = a;
//int& ra = a; - 不行,a为常量
}
5.4 引用的使用场景
①做参数
c
void Swap(int& left,int& right)
{
//...
}
//此时就等价于之前C语言用指针了
Swap(a,b);
②做返回值
- 做返回值,需要注意:不可以用局部变量做返回值
- 或者换句话说:返回的变量出了作用域或者生命周期后,不能引用返回
c
int& count()
{
int a = 0;
a++;
return a;
}
void test()
{
int x = 100;
}
int main()
{
int& b = count();
test(); // 函数调用可能覆盖了 b 指向的内存
cout << b << endl; // 可能输出随机值,不再是 1
return 0;
}

count() 函数中,a 是一个 局部变量
局部变量在函数返回时就被销毁,其内存被回收
返回 a 的引用,相当于返回了一个"野引用",指向已不存在的内存
b 绑定到这个无效的内存位置,后续使用就是访问已释放的内存
- 所以,能够做引用返回的变量有:全局、静态、堆等,都可以作为返回值
5.5 传值和传引用的效率对比

- 以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝
- 因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低
5.5 引用和指针的区别
- 语法上:引用是一个别名,没有独立空间
- 底层实现上:按照指针的方式实现

- 具体不同点
- 1.引用定义一个变量的别名,指针存储一个变量的地址
- 2.引用定义时必须初始化,指针没有要求
- 3.引用初始化一个引用一个实体后,而不能引用别的实体,指针可以指向任何一个同类型实体
- 4.没有NULL引用,有NULL指针
- 5.在sizeof中含义不同 - 引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32为平台下4个字节)
- 6.引用自己+1就是实体的值增加1,指针自加是指针向后偏移一个类型的大小
- 7.有多级指针,但是没有多级引用
- 8.访问实体的方式不同:指针需要显示解引用,引用编译器可以自己处理
- 9.引用比指针更安全
6.内联函数
6.1 概念
- 以inline修饰的函数就叫做内联函数,编译时C++编译器会在调用内联函数的地方展开

6.2 特性
- 以空间换时间:将函数按照内联函数处理,那编译阶段就会将函数体替换函数调用,减少了调用开销,但是可能会使得目标文件变大
- 不同的编译器关于inline1的实现机制可能不同,一般的建议:
- 函数的规模较小,且不是递归、不是频繁调用的函数,用inline修饰,否则编译器忽略掉了inline的特性

- 函数的规模较小,且不是递归、不是频繁调用的函数,用inline修饰,否则编译器忽略掉了inline的特性
- inline不可以声明和定义分离:因为链接的时候需要函数的地址,而inline展开后,就没有函数地址了,链接也找不到


宏
优点:
- 代码复用性高
- 可以提高性能
缺点:
- 不方便调试
- 导致代码可读性差,可维护性差
- 没有类型安全的检查
C++有没有技术可以替代宏
- 常量定义 - 用const enum
- 短小函数定义 换用内联函数
7.auto
7.1 何为auto
- auto是一个关键字
- 现在在C++11中赋予了新的标准:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器
- auto声明的变量必须由编译器在编译时期推导而得

- auto变量的定义必须初始化:因为编译阶段编译器需要根据初始化的表达式来推导auto的实际类型
- auto不是一种声明,而是一个类型声明的占位符,编译期间会将auto替换为变量的实际类型
7.2 使用细则
①可以与指针结合使用:auto <=> auto* - 两个都可以实现指针类型的声明,但是声明引用类型需要加&
c
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;

②同一行定义多个变量:变量的类型必须相同
- 编译器只对第一个类型进行推导,然后用推导出来的类型定义其他变量
c
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

7.3 auto不能推导的场景
①auto不能作为函数的参数

②auto不能直接用于声明数组

③利用auto声明的操作已经在C++11标准中删去
当然,auto也可以和C++11提供的新式for循环和lambda表达式配合使用
8.基于范围的for循环
8.1 范围for的语法
for循环后的由冒号分成两部分
- 冒号前半部分:为用于迭代的变量
- 冒号后半部分:被迭代的范围
这个for也可以用continue结束本次循环,用break跳出本次循环
c
int main()
{
int a[] = { 1,2,3 };
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
for (auto& e : a)
{
e *= 2;
cout << e << " ";
}
}

8.2 使用条件
- for循环的迭代范围是确定的:
- 对于数组而言,就是数组第一个元素和最后一个元素的范围
- 对于类而言,需要提供begin和end的方法(这两个代表for的范围)
- 迭代的对象需要实现++和==的操作才行(后续博客会提到)
9.指针空值nullptr(C++11)
9.1 C++98的指针空值
- 我们声明一个变量的时候通常会初始化,例如声明指针变量,避免野指针出现
c
int* p1 = NULL
- NULL本身其实是一个宏
c
#ifndef NULL //如果这个宏没被定义
//才进入这个代码块
#ifdef __cplusplus //C++中NULL为0
#define NULL 0
#else //C中NULL为void*的指针
#define NULL ((void *)0)
#endif
#endif
- NULL可能会被定义为字面常量0,也可能会被定义为无类型指针(void*)的常量
- 但是可能会出现一些问题:

- 程序原本想通过f(NULL)调用f(int*)的函数,但是由于NULL被定义成0的,所以无法实现此效果
- 在C++98中,如果要按照指针的方式使用,必须进行强转(void*)0才可以
- 但是现在,我们可以使用nullptr来实现这个操作:

- nullptr表示空值的时候,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- C++11中:sizeof(nullptr)和sizeof((void*)0)所占字节数相同
- 后续指针的空值表示,就都用nullptr吧