写在前面:
C++ 是在 C 语言基础之上加码完善而成的,在其学习之初,首要解决的就是 C 语言在编程上的明显缺陷。
目录
一、命名空间(域)
C 语言 问题一:无法解决命名冲突的问题(自定义名和库冲突,项目之间的自定义名冲突),还引申出域的问题
C++ 提出解决:namespace,域作用限定符
C++ 有一个基本逻辑,同一个域下,不能出现相同命名。
而创建各自的 命名空间 其实就是创建多个 域。在各自的命名空间域下,便可以定义相同的变量名,编译器不会产生报错。
访问某个 命名空间 / 域 里面成员 的三种方式:
- 指定 命名空间访问 -->
空间名::成员
::(域作用限定符) 可以精准指向 指定域 中的 成员,空格默认为全局域。- 部分 展开 -->
using 空间名::对象名
展开常用的命令。- 全局 展开 -->
using spacename 空间名
相当于把命名空间又打开为全局,平时练习可以节省时间,但一般项目里,不推荐这样做。
//【局部域、全局域 的区别】主要:1.使用 2.生命周期
int a = 2;
void f1()
{
int a = 0;
printf("%d\n", a); // 0
printf("%d\n", ::a); // 2
}
int main()
{
printf("%d\n", a); // 2
f1();
return 0;
}
------
输出结果:
2
0 --> 局部优先于全局
2 --> :: 域作用限定符,空格符" "是默认的全局域,指挥编译器从全局中找
举例:两个程序员共同编写一个项目,变量或函数等命名相同的问题,就可以通过命名空间解决啦~
//【命名空间域】 只影响使用,不影响生命周期
/******************************** AQueue.h *******************************/
/* 程序员小 A 实现的 */
namespace AQueue
{
struct Node
{
struct Node* next;
int val;
};
struct Queue
{
struct Node* head;
struct Node* tail;
};
} // 注意:命名空间不加分号
/******************************** BList.h *******************************/
/* 程序员小 B 实现的 */
namespace BList
{
struct Node
{
struct Node* next;
struct Node* prev;
int val;
};
struct List
{
struct Node* LNode;
int sz;
};
}
/******************************** Test.cpp *******************************/
#include "List.h"
#include "Queue.h"
using std::cout; // 标准库 输出 (部分展开)
using std::endl; // 标准库 换行
using spacename AQueue; // (全部展开)
using spacename BList;
int main()
{
// 指定空间访问
struct AQueue::Node nodeQ;
struct BList::Node nodeL;
// 全局展开 -- 声明后可直接访问
struct List l;
struct Queue q;
// 部分展开 -- 声明后可直接访问
cout << "hello C++" << endl;
return 0;
}
------
tips:命名空间是可嵌套的,如果命名空间也重名,里面再进行嵌套,加以区分
相当于我在公安的居民信息系统里找一个人叫凯文,同省份的有十个个、同地级市的有三个,同小区就他一个,这样层层剖析就能精准定位到我要找的小凯文
注:std 是存放 C++ 标准库的空间名。
二、流插入、流提取 运算符
C 语言 问题二:输入输出的类型复杂繁多
C++ 提出解决:cout、cin,流插入、流提取 运算符
自动识别类型,但是也有控制精度等操作很麻烦。
C++ 的使用原则,兼容 C 语言,哪个方便用哪个。
三、缺省参数
C 语言 问题三:函数设置了参数,则必须传参
C++ 提出解决:设置缺省参数,若调用函数未传参,则采用默认参数
函数定义、函数声明,只能 选择其中一个 设置缺省参数,同时给会报错。声明和定义如果分离,则只能在 声明 中设置。
缺省参数只能使用:全局变量、常量 。
比如:不能在类中,将缺省参数设置为类中定义的变量
缺省有 全缺省 和 半缺省。
// 【全缺省】
void func(int a = 1, int b = 2, int c = 3)
{
cout << "a = " << a << " ";
cout << "b = " << b << " ";
cout << "c = " << c << " ";
cout << endl;
}
int main()
{
// 不能跳着传参
func(11, 22, 33);
func(44, 55);
func(66);
func();
return 0;
}
------
输出结果:
a = 11 b = 22 c = 33
a = 44 b = 55 c = 3
a = 66 b = 2 c = 3
a = 1 b = 2 c = 3
如果是半缺省,使用缺省值,必须 从右往左 连续缺省。
使用场景:在确定数据量的情况下,栈、堆 初始化时的扩容步骤,是非必要的消耗,使用缺省参数可以极大程度的避免该种消耗。
// 【半缺省】
// 场景复现~~
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps, int defualtCapacity = 4)
{
ps->a = (int*)malloc(sizeof(int) * defualtCapacity);
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->top = 0;
ps->capacity = defualtCapacity;
// 扩容部分实现...略
}
int main()
{
struct Stack St1; // 已知最大 100 个数据
struct Stack St2; // 不确定数据量
StackInit(&St1, 100); // 给出指定参数
StackInit(&St2); // 无法指定,则使用缺省参数
return 0;
}
四、函数重载
C 语言 问题四:函数名称不允许相同,在一些实现相同,数据类型不同的情况下,只能设置多个名称不同的函数
C++ 提出解决:设置 函数重载
函数重载:同一个域里,允许定义同名函数,并由 形参列表(参数个数、参数类型、类型顺序) 来区分他们。值得注意的是,如果只是 返回值 不同,调用时是无法区分的,不能构成函数重载。
每个编译器都有自己的函数名修饰规则(由于 Windows 下 vs 的修饰规则过于复杂,而 Linux 下 g++ 的修饰规则简单易懂,笔者使用了 g++ 演示这个修饰后的名字),C++ 编译器 会将函数修饰成 【_Z + 函数长度 + 函数名 + 类型首字母】。
上述针对描述的是在同一个域里,实际上联系第一点所讲,函数名、参数类型 和 名称空间 都被加入了修饰后名称,这样编译器(还有连接器)就可以区别同名但不同类型或名称空间的函数,而不会导致 link 的时候函数多重定义。
函数重载的所有的匹配,都是在编译时完成的(匹配可理解成,找到了该函数的编译地址),所以影响的是编译速度,并不会影响运行速度。
int Add(int a, int b) // _Z3Addii
{
return a + b;
}
double Add(double a, double b) // _Z3Adddd
{
return a + b;
}
int main()
{
Add(1, 2); // call _Z3Addii(0x313131310)
Add(1.1, 2.2); // call _Z3Adddd(0x313131320)
return 0;
}
关于案例中 相似函数 重复定义的问题, 👉🔗 模板 👈 可以完美解决。
五、引用
C 语言 问题五:二级指针 + 实参形参,学 C 语言的永恒痛点
C++ 提出解决:引用 --> 形参的改变可以影响实参
引用:相当于给已经存在的变量取一个别名,即多个名字指的都是同一块空间,可以给别名取别名。
引用只能在定义时被初始化一次,之后别名绑定的对象不可变。
适用1:输出型参数
可以用在 C 语言中总是要传指针的地方,即我们希望通过形参的改变来直接改变实参。
int main()
{
// 正常赋值
int i = 0;
int j = i;
// 取别名
int& k = i;
int& m = k; // 给别名取别名也 ok
cout << &i << endl;
cout << &j << endl;
cout << &k << endl;
cout << &m << endl;
return 0;
}
------
输出结果:
00CFF744
00CFF738
00CFF744
00CFF744
使用场景 1_1
Swap() 交换函数,引用可以直接改变实参。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int i = 3;
int j = 1;
cout << "交换前:i = " << i << " j = " << j << endl;
Swap(i, j);
cout << "交换后:i = " << i << " j = " << j << endl;
return 0;
}
------
输出结果:
交换前:i = 3 j = 1
交换后:i = 1 j = 3
使用场景 1_2
数据结构许多场景都可以使用。
//【1】
typedef struct Node
{
struct Node* next;
int val;
}Node;
void PushBack(Node*& phead, int x)
{
Node* newNode = (Node*)malloc(sizeof(Node));
if(phead == nullptr)
{
phead = newNode;
}
// 其余实现省略...
}
int main()
{
Node* plist = NULL;
PushBack(plist, 1);
PushBack(plist, 2);
return 0;
}
// 【2】
// 通过前序遍历的数组 “ABD##E#H##CF##G##” 构建二叉树
// BTNode* BinaryTreeCreate(char* a,int* pi)
BTNode* BinaryTreeCreate(char* a,int& ri)
{}
int main()
{
char* ptr = "ABD##E#H##CF##G##";
int i = 0;
BinaryTreeCreate(ptr, i);
return 0;
}
适用2:返回值
注意:函数返回时,出了函数作用域,如果返回对象还在(未被系统收回),则可以使用引用返回,如果还给系统了,必须使用传值返回。 可以使用的比如:静态、全局、上一层栈帧、malloc 的…
两个作用:
1. 减少了拷贝(提高了程序效率)
2. 调用者可以直接修改返回对象
使用场景2_1
static 静态变量
// 【传值返回】
// n 虽然储存在静态区,不随着函数栈帧的销毁而销毁
// 但在调用时,ret 接受的返回值,仍然不是 n 直接给它的,是在 main 栈帧里面提前开的一块空间放临时变量传递的(如果临时变量小,也可能是直接通过寄存器储存的)
int Count()
{
static int n = 0;
n++;
return n;
}
// 【传引用返回】
// 这里的返回值,实际是通过 n 的别名也就是 n 的那块空间的值,减少了一次拷贝
// 相当于编译器把这一步的控制权交给了程序员
int& Count2()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
ret = Count2();
return 0;
}
适用场景2_2
访问静态数组
/************************ 函数写法 ***************************/
#define N 10
typedef struct Array
{
int a[N];
int size;
}AY;
// 访问第 i 个数据
int& PosAt(AY& ay, int i)
{
assert(i < N);
return ay.a[i]; // 出了 PosAt 栈帧还在,所以可以用引用返回
}
int main()
{
AY ay;
for (int i = 0; i < N; i++)
{
PosAt(ay, i) = i * 10;
}
for (int i = 0; i < N; i++)
{
cout << PosAt(ay, i) << " ";
}
return 0;
}
/********************** 结构体写法 *************************/
// 后面用类,会更加简洁
#define N 10
typedef struct Array
{
int& at(int i)
{
assert(i < N);
return a[i];
}
int a[N];
int size;
}AY;
int main()
{
AY ay;
for (int i = 0; i < N; i++)
{
ay.at(i) = i * 10; // 修改返回对象
}
for (int i = 0; i < N; i++)
{
cout << ay.at(i) << " "; // 访问
}
cout << endl;
return 0;
}
一个错误案例:
int& Addtion(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Addtion(1, 2);
Addtion(3, 4);
cout << "Addtion(1, 2) is :" << ret << endl;
cout << "Addtion(1, 2) is :" << ret << endl;
return 0;
}
// 输出结果:
//Addtion(1, 2) is :7
//Addtion(1, 2) is :2043451784 --> 随机值
// 分析:
// add 函数中的 c 出了作用域都销毁了,引用他干嘛呀
// 语法上可以,不代表程序是对的,其结果无法定义~
const 权限问题
善用使用 const,可以极大程度保证被引用对象的值不被失误篡改
引用和指针一样,赋值 / 初始化 的权限可以 缩小 或 持平,但是 不能放大
int CountN()
{
int n = 0;
n++;
return n;
}
int main()
{
/****************** 【权限放大】 err.. *****************/
/*
const int a = 2;
int& b = a;
const int* p1 = NULL;
int* p2 = p1;
*/
/****************** 【权限平移 / 保持】 *****************/
const int c = 2;
const int& d = c;
const int* p3 = NULL;
const int* p4 = p3;
const int& ret = CountN();
int i = 0;
const double& rd = i; // 只要类型转换会产生临时变量
/********************* 【权限缩小】 *********************/
int x = 1;
const int& y = x;
int* p5 = NULL;
const int* p6 = p5;
/*---------- 这只是单纯的赋值,不影响 m 的权限 -------------*/
const int m = 1;
int n = m;
return 0;
}
补充
引用在语法的角度上来看,是不会开辟空间的。
但在汇编层面看,其实也是用指针实现的,具体汇编原理不是 C++ 学习重点,这里便不赘述。他较指针更为安全,使用也更加便捷。
六、内联
C 语言 问题六:宏缺点太多 --> 1.不能调试 2.没有类型安全的检查 3.有些场景下非常复杂。
(宏函数更是一种暴力替换,可移植性极低)
C++ 提出解决:推荐使用 const 和 enum 替代宏常量,用 inline 去替代宏函数
保留了宏的可维护性、不限制数据类型、不会开辟栈帧的优点
宏的回顾
// 宏函数 实际上就是暴力替换,多一个分号都会出岔子...
// 实现 ADD 宏函数(写时需谨慎)
// 两层括号,分别什么作用? --> 请深刻理解“暴力”替换有多“暴力”
#define ADD(x,y) ((x)+(y))
int main()
{
ADD(1, 2) * 3; // (1+2)*3 --> 外层大括号的作用
int a = 1, b = 2;
ADD(a | b, a & b); // ((a | b) + (a & b)) --> 内层小括号的作用
return 0;
}
inline
内联函数:以 inline 修饰 的函数,编译时 C++ 编译器在 调用内联函数的地方展开,不会有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
inline int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int ret = Add(1, 2); // 编译器会在这里展开内联函数
cout << ret << endl;
return 0;
}
我们一般期望将 规模较小、不是递归、且频繁调用 的函数采用 inline 修饰,因为他是一种 用编译空间换时间 的方法,虽然可以提高程序的运行效率,但是可能会使目标文件变大。
打个比方:
一个程序有 1000 个地方调用了 Swap 函数,若 Swap 函数编译起来有10行,则
1. Swap 是普通函数
Swap + 调用 Swap 指令,合集指令行 --> 1000 + 10
2. Swap 是 inline 函数
Swap + 调用 Swap 指令,合集指令行 --> 1000 * 10
在实际编译中,内联只是向编译器发出的一个请求,编译器可以选择忽略这个请求…
inline 函数 的声明和定义
inline 函数的 声明和定义 ,需要 放在一起
先看现象:
/******************** Func.h **********************/
inline void f(int i);
/******************* Func.cpp *********************/
// 将函数定义与声明分开,放在这个文件中,会报错:error LNK2019 无法解析的外部符号,也就是链接错误
#include "Func.h"
inline void f(int i)
{
cout << i << endl;
}
/******************* Test.cpp *********************/
#include "Func.h"
int main(){
f(1);
return 0;
}
------
修改建议:直接将 inline 函数在 .h 文件中定义即可
成因分析:
正常来说:函数调用需要 call 一个地址,如果在展开的 Func.h 文件中,只有声明没有定义。
编译器就会拿着编译后产生的函数名,去包含声明头文件的文件中找该函数,并完成调用。
将内联函数声明定义分开在两个文件中,报错:链接错误 --> 链接不上,意思就是找不到
原因:inline 函数是不进符号表的,他不会产生函数地址
函数地址又是个啥?
解释:是调用的时候 call 的一串符号,这串符号其实是一个用作编译跳转的地址,作用是转到编译 jmp 创建函数栈帧的地址
inline 函数直接展开,根本不会开辟函数栈帧,所以也不会产生函数地址
七、auto
C 语言 问题七:赋值变量的时候,有时无法清楚的知道表达式的类型
C++ 提出解决:用 auto 声明变量,直接由编译器推导而得
int main()
{
// 简单举例
int a = 0;
auto b = a;
auto c = &a;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
// 复杂举例 (后期会讲到的 迭代器)
std::map<std::string, std::string> dict;
//std::map<std::string, std::string>::iterator dit = dict.begin();
auto dit = dict.begin(); // 跟上一行作用一样
}
------
输出结果:
int
int *
typedef 是有缺陷的,这里使用 auto 更准确和简洁。
使用事项
auto 可以在一行定义多个变量,但这些变量必须是相同的类型,否则编译器会报错。因为编译器实际只对第一个类型进行推导,用推导出来的类型定义其余变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0; // err...编译失败
auto 不能做形参,不能声明数组
再体会一个 auto 的便捷用法
数组遍历:范围 for
自动依次取数组中的数据赋值给 e 变量,自动判断结束
int arr[] = { 1,2,3,4,5,64,3,2 };
/******************** 遍历的常规写法 ********************/
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
cout << arr[i] << " ";
}
cout << endl;
/******************** 范围 for 写法 ********************/
/*----------- 打印 2 倍的数组,但是不改变原数组 -----------*/
for (auto e : arr) // 这里的 auto 也可以自己选择声明别的类型,e 是一个自定义的变量名,起什么都行
{
e *= 2;
cout << e << " ";
}
cout << endl;
/*--------------------- 查看原数组 --------------------*/
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
/*------------------ 遍历改变原数组 --------------------*/
for (auto& e : arr) // 引用!!!
{
e *= 2;
}
/*--------------------- 查看原数组 --------------------*/
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
------
输出结果
2 4 6 8 10 128 6 4
1 2 3 4 5 64 3 2
2 4 6 8 10 128 6 4
八、nullptr 和 NULL
C 语言 问题七:NULL 在 C++ 中被定义成 0
C++ 提出解决:nullptr
在使用 nullptr 表示空指针时,不需要包含头文件,因为他是 C++11 作为新关键字引入的。
在 C++11 中,ziseof(nullptr) 和 sizeof((void*)0) 所占字节数相同。
为了提高代码的健壮性,表示空指针时最好使用 nullptr。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL); // c++ 里面的 NULL 被设置成了 0
ff((int*)NULL);
ff(nullptr);
return 0;
}
------
输出结果:
f(int)
f(int)
f(int*)
f(int*)
🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~