🎇个人主页:Ice_Sugar_7
🎇所属专栏:C++启航
🎇欢迎点赞收藏加关注哦!
文章目录
- 🍉前言
- 🍉命名空间
- 🍌访问命名空间中的元素
- 🍌同名命名空间
- 🍌展开
- 🥝指定展开
- 🍉io流
- 🍌基本的输入输出
- 🍉缺省参数
- 🍌使用规则
- 🍉函数重载
- 🍌补充
- 🍉引用
- 🍌注意事项
- 🍌常引用
- 🍌相关应用
- 🥝传参
- 🥝作为返回值
- 🍌引用的底层实现
- 🍌引用与指针的区别
- 🍉内联函数
- 🍌基本概念
- 🍌注意事项&建议
- 🍉关键字:auto
- 🍉范围for循环
- 🍉指针空值nullptr
🍉前言
cpp在绝大部分情况下都兼容C语言,相当于C语言的拓展,所以在正式进入cpp的世界之前,我们得先了解相较于C语言,cpp有哪些不同之处,然后才能入门。
🍉命名空间
在写代码的过程中,我们自己写的函数可能与库函数冲突(函数名相同),而在一个项目中,我定义的某个类型或者函数有可能和你的定义一样,我们单独运行自己的代码没问题,但是一旦合并到一个项目中运行就会产生冲突。
这时除了改变我们两人其中一个的代码的名字,我们可以使用命名空间,它的形式如下:
namespace space {int a = 0;
}
类似结构体,但是大括号后面没有分号,里面可以存放变量、函数、某种类型等
命名空间相当于一堵墙,将其中的元素与全局隔离开了,在找变量的时候,会去找全局中的变量,哪怕找不到,也不会去找命名空间中的。
🍌访问命名空间中的元素
访问的话很简单,只需按照命名空间名 + :: + 你想访问的元素
的格式就ok,例子如下:
namespace space {int c = 10;
}int a = 10;printf("%d\n", space::c);
🍌同名命名空间
多个文件中含有同名的命名空间,它们不会产生冲突。
比如你在头文件里面定义了一个命名空间,源文件里面也有一个同样的命名空间,这个源文件在包含头文件的时候,编译器会自动识别将它们进行合并,因此不会发生冲突。
🍌展开
展开就相当于你把这堵围墙给拆了
展开后搜索顺序
我现在同时展开两个命名空间,又定义了一个变量,那会如何搜索呢?
using namespace a{//...};using namespace b{//...};
规则是这样的:先搜索全局,全局找到了就不会搜索这两个命名空间;如果找不到再来它们里面找,从上往下找,即先找a,a找不到再找b,a找到的话那自然不用去b里面找咯。(都找不到的话那就报错了)
拓展:
头文件展开:在预处理阶段把头文件的内容拷贝过来
命名空间展开:默认访问命名空间中的变量而非全局中的变量
只要你展开了,那默认就会去命名空间中搜索,不用每次都要去指定。(要指定也可以,只不过一般不那么做)
那接下来我们来看下它是啥意思
using namespace std;
std是cpp官方库定义的命名空间,cpp库里面的东西都在它里面,你把它展开之后就可以使用cpp的东西了
注意:以后在工程项目中,不要随便展开std,因为很有可能会与里面的函数发生冲突(不然你想下为啥要把它们封装在std里面),而现在日常的练习之中,由于代码量比较小,发生冲突的概率很小,所以一般就直接展开了。
🥝指定展开
不展开的话每一次都要指定很麻烦,全部展开的话又会有冲突的风险,那怎么办呢?这时候就要用到指定展开
。
要用啥就展开啥,只展开一部分才是最稳妥的方式
using space::add; //只展开space中的函数add,此时可在任意位置使用add
🍉io流
我们在cpp中通常会包含这样一个头文件:
#include<iostream>
里面包含cin、cout、<<等输入输出函数,相当于C语言的stdio.h,然后注意cpp中包含io流不用写“.h”
在C语言中 << 是一个位运算符,在cpp中它有了新的含义一一流插入运算符
;相应地,>>叫做流提取运算符
。
(说到流入和提取,以及看到 in 和 out,不禁回想起之前学习文件操作时接触到的“输入流”和“输出流”,输入流就是把流里面的数据给到文件,而输出流则是把数据从文件中提取出来,放到流里面。)
🍌基本的输入输出
using namespace std;int main(){int a = 10;cout << a << endl;int b;cin >> b;cout << b << endl;int c, d;cin >> c >> d; //输入两个数cout << c << ' ' << d << endl; //输出c,d并用空格分隔return 0;
}
输出结果:
cpp中的cout会自动识别类型,不用和printf一样还要去区分%d,%lf 什么的,比如上面把c改为浮点型,照样可以正常输出。
关于控制浮点数位数以及左、右对齐:这两个点用cin和cout不好实现,但是我们可以用printf,毕竟cpp兼容C语言,两个混用也是ok的,哪个方便用哪个就是了。
🍉缺省参数
以前学C语言的时候,如果函数如果有形参,那我们传参时就要传个参数过去。而C++中有了缺省参数这个概念,我们可以选择不传参,此时缺省参数会作为参数进行传参。其实说白了它就相当于一个备胎,实参部分有参数时就轮不到它,没有的时候才能上场。
int mul(int a = 10, int b = 20) {return a * b;
}int main() {int a, b;cin >> a >> b;cout << mul(a, b)<< endl;cout << mul() << endl; //使用缺省参数
}
🍌使用规则
使用缺省参数时需注意:
①只能自右向左缺省,若有多个缺省参数,则必须连续缺省,比如下面两个函数声明就犯了这种错误。
int func1(int a = 10, int b, int c); //不是从右向左缺省
int func2(int a = 10, int b,int c = 20); //没有连续缺省
②函数不能在声明和定义同时出现缺省参数
这样规定的理由其实很好理解,要是你声明函数和定义函数时设置的缺省参数不一样的话,那就不知道要选谁作为缺省参数了。(一般函数声明和定义不在同一个文件)
③缺省参数值只能为常量或者全局变量
🍉函数重载
cpp支持函数重载,在函数名相同的情况下,只要参数不一样,那么这两个函数就可以共存。
参数不同包括:①参数类型不同;②参数个数不同
比如
int func(int x, char y);
int func(char x, int y);
C语言在链接时是通过函数名去寻找函数地址,所以不允许出现同名函数;而cpp中采用函数名修饰规则,链接时根据修饰后的函数名去寻找函数的地址
在Linux环境下,通过指令 objdump -S 可执行程序可以查看函数名修饰后的情况。
比如,对于下面这个函数:
void func(int a,double b) {//...
}
如下图,可以看到它的函数修饰名为_Z4funcid
其中_Z为函数修饰名前缀
;4表示函数名所占的字符数
;i
表示整型参数;d
表示双精度浮点型的参数。同时你会发现函数修饰名中没有包含返回值
,这也说明两个函数仅有返回值不同是无法构成重载的。
🍌补充
将一个源文件转化为可执行程序(.exe)需要经过预处理、编译、汇编和链接四个步骤
预处理阶段会进行头文件展开、宏替换、条件编译(去除条件语句中不用执行的语句)、去除注释等操作
编译阶段会检查语法,生成汇编代码,编译错误就是因为语法出错
汇编阶段会将汇编代码转换为二进制的机器代码
链接阶段会将各个文件合并起来,链接一些还没有确定的函数地址
将代码转到反汇编可以看到汇编代码,如下:
其中的push、mov、sub都是我们可以看懂的,但是机器只认识二进制代码,所以汇编阶段要将它们转为二进制。
🍉引用
引用是给一个已经存在的变量再起一个“别名”,它和被引用的变量共用同一块内存空间
(如上图,b就是a的引用)
🍌注意事项
①引用必须初始化,即不能直接 int& b;
②一个变量可以同时有多个引用。毕竟是别名,多起几个也没事儿。
int a = 1;int& b = a;int& c = b;int& d = a;
③引用无法改变指向。如下图,比如b是a的指向,那么就不能再让b成为c的引用。也正是这个特性,使得引用无法完全替代指针
🍌常引用
我们都知道,常量不可以被修改,但是引用可以对原数据进行修改,所以直接对常量进行引用就相当于放大了权限(从不可修改变为可以修改),编译器会报错。想引用常量的话,只需在引用前面加上const
就ok了。
void TestConstRef()
{const int a = 10;//int& ra = a; // 该语句编译时会出错,a为常量const int& ra = a;// int& b = 10; // 该语句编译时会出错,b为常量const int& b = 10;
}
🍌相关应用
🥝传参
在实现单链表的尾插时,我们说当链表为空,想插入节点要传链表的二级指针,这样才能改变节点。但是这样确实很麻烦,也不好理解。而对于引用,由于它作为函数形参时,和实参共用同一块空间,所以改变它会影响实参,比传二级指针简洁多了。下面以这个函数为例进行对比
指针
void SLPushBack(SLNode** pphead,SLTypeDate x) {assert(pphead); //进行断言,防止传过来的指针为空SLNode* node = SLBuyNode(x);if (*pphead == NULL) { //phead为空说明此时链表为空,那就直接插入*pphead = node; //让node是第一个节点的地址return;}//若不为空,则先找到链表的最后一个节点,再插入SLNode* pcur = *pphead; //老样子,用临时变量pcur去走循环while (pcur->next) {pcur = pcur->next;}//此时pcur指向最后一个节点pcur->next = node;
}
引用
void SLPushBack(SLNode*& phead, SLTypeDate x) {assert(phead); SLNode* node = SLBuyNode(x);if (phead == NULL) { phead = node; return;}SLNode* pcur = phead; while (pcur->next) {pcur = pcur->next;}pcur->next = node;
}
🥝作为返回值
传值返回首先会对返回值进行拷贝,然后返回这份拷贝;对于传引用返回,也就是返回n的别名(你不用去理会这个别名叫什么,编译器会自行处理的,或者直接理解为返回n)。那此时到底会返回什么呢?这取决于栈帧结束后编译器会不会将栈帧内的值置为随机值并清理掉。我们来做个测试看看vs2022的编译器是否会进行清理。
int& test1(int i) {int data = i;return data;
}int main() {int& ret = test1(10);cout << ret << endl;return 0;
}
由此可见,vs2022在函数栈帧结束后不会将栈帧中创建的变量清理掉
🍌引用的底层实现
在语法概念上,指针有开辟一块空间,而引用没有独立的空间。
不过对于这段代码,我们进行调试并转到反汇编观察一下。
int x = 1;int* px = &x;int& y = x;
可以看到,引用和指针的汇编代码一模一样,说明引用在底层实现实际上是有开辟空间的,因为引用是按照指针的方式来实现的,即引用的底层是通过指针实现的。
尽管如此,你也不要与上面的语法概念产生混淆,因为语法概念是上层的,而汇编是底层的。我们要按照语法概念来理解,默认引用就只是个别名。
🍌引用与指针的区别
在现阶段大部分场景下,只要不涉及改变指向,就基本可以用引用代替指针。下面总结一下二者的区别(有一些上面已经讲过了,就不再赘述):
有NULL指针,但没有NULL引用,
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
有多级指针,但是没有多级引用
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
引用比指针使用起来相对更安全(比如说有野指针,但是没有野引用)
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
🍉内联函数
🍌基本概念
用inline修饰的函数叫做内联函数,在编译阶段
,C++编译器会在调用内联函数的地方展开,用函数体替换函数调用,没有函数调用建立栈帧的开销,可以提升程序运行的效率。
注意:在release模式下才会展开,debug模式下仍然会调用函数(因为debug模式下,编译器默认不会对代码进行优化)。
inline int Add(int x, int y) {return x + y;
}int main() {cout << Add(1, 2) << endl;return 0;
}
在debug模式下观察汇编代码:
在release模式下,没有跳转到Add函数的指令:
🍌注意事项&建议
假设现在要实现一个栈,那就需要有Stack.h、Stack.cpp和test.cpp三个文件。
如果声明时在函数前面加上inline使其变为内联函数(假设在编译时这个函数会展开),那么会产生什么影响呢?
对于test.cpp文件,里面只有包含的头文件(函数声明)和函数的调用,在链接的时候需要去找函数的地址(链接时每个cpp文件生成的.o文件中都有一个符号表),但是内联函数的地址是不会进入符号表的,所以也就找不到它的地址了,就会产生链接错误了。
对于Stack.cpp文件,因为它本身就有函数的实现,编译时函数体直接展开,因此不需要得到函数的地址
所以我们可以得到这样的结论:一个内联函数如果声明和定义不在同一个文件的话,那么你就不能在其他文件(除Func.h 和 Func.cpp 之外的文件)中调用它
下面是一个链接错误的例子:
// 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);return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
内联确实可以提高运行效率,但是不可乱用,比较大的函数采用内联直接展开的话会增加占用空间,使生成的可执行程序变大。为了防止程序员滥用,内联函数最终是否展开取决于编译器的处理,也就是说内联相当于是向编译器发出请求,编译器可以执行,也可以忽略。一般较大的函数即使加了inline,编译器也会忽略,不会直接展开。
所以,建议代码量较小的
且频繁调用的
函数在前面加inline。
🍉关键字:auto
随着程序越来越复杂,程序中用到的类型也越来越复杂,体现在:
①类型难于拼写
②含义不明确导致容易出错
为了解决这些问题,C++11中,auto可以识别右值的类型并自动转换为相应的类型。
在愉悦地上手auto之前,有如下的注意事项:
auto定义变量时必须初始化
用auto声明指针类型时,用auto和auto*没有任何区别,但是用auto声明引用类型时必须加&
int main()
{int x = 10;auto a = &x;auto* b = &x;auto& c = x;cout << typeid(a).name() << endl; //获取变量a的类型,了解即可cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;*a = 20;*b = 30;c = 40;return 0;
}
在同一行定义多个变量时,变量类型必须相同,否则编译器会报错。因为编译器只会对第一个变量进行推导,然后用推导出来的类型去定义其他变量
void TestAuto()
{auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能做函数参数,也不能做返回值
🍉范围for循环
C++11中引入了基于范围的for循环,for循环后的括号由冒号:
分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示迭代的范围。
光看文字肯定看不太懂,看下程序就明白了
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };for(auto& e : array) //一般用auto,这样数组类型改变的时候你不用去修改e *= 2;for(auto e : array)cout << e << " ";return 0;
}
对于第一个范围for循环,就是依次取数组中的元素,然后赋值给e,再让e乘2,因为这里使用了引用,所以改变e就会改变数组中的元素。数组第一个元素乘2之后会自动往后走(相当于i++),让第二个元素赋值给e之后乘2…直到最后一个元素,然后自动判断结束。
如果不使用引用,写为for(auto e:array)
的话,在循环中让e*2不会改变数组中的数据,因为只是把数组的值赋给e,改变e当然没法改变数组元素的值咯。
范围for相比与以前for循环,最大的进步就是不用自己去判断循环的终止条件了,这样既节省了时间,也可以避免写错终止条件而出错。
照例还是有一些注意事项:
①范围for中的数组名指的是整个数组(类比sizeof(数组名)),所以数组作为函数参数时,不能在函数中使用范围for,因为此时数组名表示的是首元素地址
②与普通循环类似,可以用continue
来结束本次循环,也可以用break
来跳出整个循环
③范围for循环迭代的范围必须是确定的(对于数组而言,就是数组中第一个元素和最后一个元素的范围)
④迭代的对象要实现++和==的操作
🍉指针空值nullptr
在C++中,NULL被编译器识别为0而非void*
可以用以下程序证明:
void TestNULL(int* p) {cout << "int*" << endl;
}void TestNULL(int p) {cout << "int" << endl;
}int main() {TestNULL(NULL); //根据输出结果判断NULL的类型return 0;
}
如果NULL是0的话,那么用它来给指针置空的话,很可能会出问题。
为了修补这个漏洞,C++11中引进nullptr
专门给指针置空。因此以后对于指针类型的变量,如果我们要初始化为空,就用nullptr
注意事项:
①因为nullptr是C++11新的关键字,所以在使用nullptr表示指针空值时,不需要包含头文件,
②在C++11中,nullptr
与void*
所占的字节数相同
③为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr