目录
3.利用using namespace+命名空间名称将其直接展开
先来看看c++祖师爷的照片
本章的C++基础都是建立在C语言地基下,祖师爷对C语言觉得不合适的地方,进行的修改.
一.命名空间
1.引入
我们先看一段简单的程序
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
//C语言没有办法解决类似的命名问题
int main()
{
printf("%d\n",rand);
return 0;
}
运行后,程序会崩溃,然后出现上面的报错.
其实我们也很好理解,由于我们包含了stdlib.h这个头文件,在链接的时候,由于原先的库函数中
也有rand这个同名的函数,因此编译器无法识别rand是函数还是我们所希望的变量,因此程序发生
了崩溃.
这看上去是一个小的问题,但是在生活却很常见.
假如一份任务被划分给不同小组去完成不同的部分,在最后阶段,需要把所有人的代码合并起来,
这时候,我们就无法保证不同小组定义的变量名是否会一样(发生冲突).
但是C语言中有一种场景,允许我们变量名是相同,但不会发生报错.
#include <stdio.h>
#include <stdlib.h>
//域
//局部域/全局域:1.使用2.生命周期
void f1()
{
int a = 0;
//默认局部优先
printf("f1函数的a:%d\n", a);
printf("f1函数的a的地址:%p\n", &a);
}
int main()
{
int a = 1;
printf("main函数的a:%d\n", a);
printf("main函数的a的地址:%p\n", &a);
f1();
return 0;
}
先看上面这段程序,可以得到如下结果.
我们注意到在函数f1中的局部变量a和main函数中的局部变量a地址并不相同,
也就是说它们并不是同一个变量,但它们是可以重名的,这是为什么呢?
这就涉及一个很重要的概念——域.
我们把f1函数看作是一个域,里面创建的变量a(局部变量),它只能在函数f1中出现,并且使
用,一旦f1函数调用结束,它就会被销毁.
同理,main函数中的变量a也是如此,它也是一个局部变量,它只能在main函数中出现,又由于
main函数在整个程序结束的时候,才会被销毁,所以main函数的变量a会一直存在,直到程序结
束.
就像一个个小的平行世界一样,即便它们拥有着相同的名字,它们一生的经历也并不相同(使用不
同,f1中的a被赋值为0,main函数中的a被赋值为1),生命长短也不同(生命周期不同).
可见,域影响了变量的使用,和变量的生命周期.
C++的命名空间也是如此,我们可以使用namespace关键字创建自己的命名空间.(域)
以此完成对标识符和名称进行本地化,以避免命名冲突或名字污染的目的
2.正常命名空间定义
使用方法也很简单,namespace,后面跟命名空间的名字,然后接一对{}即可,{}中即为命
名空间的成员(可以是变量,也可以是函数)
比如我们之前链表和队列数据结构,都有Node这个结构体.
我们将其划分在不同的命名空间AQueue和BList中,就可以重名,而不需要在命名上下一番功夫进
行区分.
namespace AQueue
{
struct Node {
struct Node* next;
int val;
};
struct Queue {
struct Node* head;
struct Node* rear;
int size;
};
}
namespace BList {
struct Node {
struct Node* next;
struct Node* prev;
};
}
PS:虽然和域的概念很像,但严格意义来说,命名空间只能算是影响了变量的使用,两两变量处
在不同的命名空间下可以重名,但是生命周期并没有改变.
3.其它定义方法
除了普通定义外,命名空间还支持嵌套定义与合并.
//嵌套定义
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
当同一个工程中允许存在多个相同名称的命名空间,编译器最后会将其成员合成在同一个命名空间
中.
假如还另外存在同样称为N1的命名空间,那编译器会将里面的内容,和另外N1的命名空间合并.
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
4.命名空间的使用
1.加命名空间和作用限定符::
//Queue.h
#pragma once
//命名空间 -- 命名空间域,只影响使用,不影响生命周期
namespace AQueue
{
struct Node {
struct Node* next;
int val;
};
struct Queue {
struct Node* head;
struct Node* rear;
int size;
};
}
//List.h
#pragma once
namespace List {
struct Node {
struct Node* next;
struct Node* prev;
};
}
#include "List.h"
#include "Queue.h"
int main()
{
//命名空间名字如果还重合,则还是会报错
struct AQueue::Node node1;
struct List::Node node2;
return 0;
}
2.利用using将命名空间的某个成员引入
#include "List.h"
#include "Queue.h"
//2.局部展开,一般情况下,建议局部展开最常用的成员
using AQueue::Node;
int main()
{
struct Node node1;
struct List::Node node2;
return 0;
}
3.利用using namespace+命名空间名称将其直接展开
#include "List.h"
#include "Queue.h"
//3.全局展开,一般情况,不建议全局展开
using namespace AQueue;
int main()
{
struct Node node1;
struct Queue q;
return 0;
}
二.C++输入&输出
在C语言中我们有标准输入输出函数scanf和printf,而在C++中我们有cin标准输入和cout标准输出.
在C语言中使用scanf和printf函数,需要包含头文件stdio.h.
而在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间
相较于scanf,printf函数而言,cin和cout一个最大的好处,是其可以自动识别类型.
#include <iostream>
using namespace std;
int main()
{
// << 流插入 >> 流输出
// endl等价于'\n'
int n = 0;
cin >> n;
double* a = (double*)malloc(sizeof(double) * n);
if (a == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
//自动识别类型输入
for (int i = 0; i < n; ++i)
{
cin >> a[i];
}
//自动识别类型输出
for (int i = 0; i < n; ++i)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
三.缺省参数
1.概念介绍
解决完C语言命名重叠问题后,C++祖师爷又将目光投向了函数参数设计上.
在栈的设计一节中,我们默认初始创建栈的空间为4个元素,不够再继续扩容.
但假如我们一开始就知道需要创建空间的大小呢?如果再不断进行扩容,会导致程序的效率底下,
于是,C++祖师爷设计了缺省参数的概念.
简单而言,就是在设计函数的时候,我可以指定默认初始值,如果用户提供自己的默认初始值,那
就按照用户提供的进行调用函数,否则,就按照我们的进行调用函数.
#include <iostream>
using namespace std;
struct Stack {
int* a;
int top = 0;
int capacity = 4;
};
void StackInit(struct Stack* ps,int defaultCapacity = 4)
{
ps->a = (int* )malloc(sizeof(int)*defaultCapacity);
ps->top = 0;
ps->capacity = defaultCapacity;
}
//缺省参数,使用缺省值,必须从右往左缺省
int main()
{
Stack st1;
//按1000进行初始化
StackInit(&st1, 1000);
Stack st2;
//用户没有提供,按照设计的4进行初始化
StackInit(&st2);
return 0;
}
2.缺省参数分类
1.全缺省
顾名思义,全缺省也就是,所有函数形参都有默认初始值
void Print(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
2.半缺省
只有部分函数形参具有默认初始值(比如下面代码中的a就没有默认初始值)
void Print(int a, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
小细节:
1.半缺省参数只能从右往左依次给出
像下面的代码,c没有默认初始值,即便调用Print函数时,已经提供三个参数,程序依旧会发生报
错.
#include <iostream>
using namespace std;
//错误示例,没有从右往左提供缺省值
void Print(int a = 10, int b = 20, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Print(1,2,3);
return 0;
}
2.缺省参数不能在函数声明和定义重复出现
假如定义和声明提供的缺省值不同,则编译器无法识别按照哪个缺省值作为默认初始化.
为了避免这种情况,缺省参数只能在声明或者定义中出现.
3.缺省值必须为常量或者全局变量
//正确示例
int x = 30;//全局变量
void Print(int a, int b = 20, int c = x)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
四.函数重载
1.概念介绍
在设计函数名字上,祖师爷也有自己的想法.
比如我们设计一个加法函数,由于C语言的类型不同,我们需要设计很多个不同名字的函数,就为
了实现不同类型的加法操作,但这些函数实际上结构非常相似,仅仅是类型不同.
//整型加法
int Add_int(int x, int y)
{
return x + y;
}
//浮点数加法
double Add_double(double x, double y)
{
return x + y;
}
...
于是祖师爷就引入了函数重载的概念,简单点讲,上面的加法函数都可以用同一个名字Add,在调
用的时候,编译器会自动识别类型,找到对应的正确Add函数.
#include <iostream>
using namespace std;
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl; //打印1+2的结果
cout << Add(1.2, 2.3) << endl;//打印1.2+2.3的结果
return 0;
}
2.原理——名字修饰
在程序环境和预处理一节中,我们提到过C语言源文件的相应的处理方法,这里简单复习一下.
对于每一个.c源文件,都会经过预处理,编译,汇编三大过程,形成对应的目标文件(.obj)
在汇编阶段,每个源文件都会形成相应的符号表,符号表里面出现的符号,就是我们的函数名,全
局变量名之类的,并给我们每个对应的符号分配相应的内存地址.
在最后链接的阶段,不同符号表中的相同符号会合并.
但假如我们一个源文件里面,形成的符号表里面出现两个同样名字的Add函数,链接器不知道该如
何链接函数地址,此时就会发生报错.
C++则不会出现类似的问题,本质在于,它形成的符号表的名字会有相应的修饰,会根据这
个新的函数名字,去链接相应的函数地址.
简单验证下结论,我们将Add函数的定义直接改为声明,这样目标文件就无法链接,会发生相应的
报错.下图就是vs2019IDE的报错提示:
可以看到,我们的Add函数,在符号表里面的名字,不再是单纯的函数名
而是变成了?Add@@YANNN@Z这个新的符号.
同样我们也可以在LINUX系统下进行验证
g++编译后,利用objdump + -S + 可执行程序,即可观察到对应函数符号.
原本的Add函数被修饰为_Z3Adddd这个新的符号.
linux下名字修饰规则也比较好懂,就是_Z+函数名字长度(3)+函数名(Add)+参数类型(d d)
知道规则,也可以明白为什么func1这个函数,在linux下会被修饰为_Z5func1id这个新符号.
PS:指针类型会加一个P,比如int* 对应为Pi
3.不同的函数重载类型
基于函数名的修饰原理,我们就可以进一步理解函数重载有什么类型.
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(double left, double right)" << endl;
return left + right;
}
2.参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
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;
}
注意:返回类型不同,不能构成重载.
按照函数名修饰原则,两者符号是相同的,就算是不同的,那编译器又怎么知道用户调用的时候,想要返回哪个类型呢?
五.引用
1.概念介绍
在链表的实现时,尾插这个函数设计,我们需要传二级指针进去.
原因在于假如链表为空的时候,我们必须改变头指针,而想要改变一级指针,就必须传二级指针进
去.
类似的场景还有挺多,比如Swap函数的交换.
于是祖师爷为了提高程序的可读性和可写性,这次将目光投向了这里,并引入了引用的概念.
引用,通俗点讲,就是起别名.
比如李逵有很多外号,像黑旋风,铁牛等等.
那你无论是叫哪个称呼,都指的是同一个人.同样的道理,假如给a变量起了个别名就做pa,则无论
是变量a还是变量pa,指向的都是同一个人.
2.用法
类型& 引用变量名(对象名) = 引用实体
3.应用特性
1.引用必须初始化使用
int a = 10;
int& b = a;//引用在定义时必须初始化
2.一个变量,可能有多个引用
#include <iostream>
using namespace std;
int main()
{
//都是变量a
int a = 1;
int& pa = a;
int& ppa = pa;
int& k = a;
return 0;
}
3.一旦引用了一个变量,就不能再引用其它变量
#include <iostream>
using namespace std;
int main()
{
//Error
int a = 1;
int b = 2;
int& pa = a;
int& pa = b;
return 0;
}
4.使用场景
1.输出型参数
形参的改变影响实参
//交换函数
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
2.返回值
传引用返回
我们知道函数在调用结束后会被销毁,里面的内容,我们就没有相应权限进行访问.
这就类似于我们在酒店开了一间房间,住了一晚后,我们要退房,此时我们归还房间钥匙后,虽然
房间依旧存在,但我们就再也没有权利访问.
因此,函数如果有返回值,实际上并不是直接返回.
而是创建一个临时变量,将结果存进去,使用的时候,再把相应函数返回值拿出来.
(ps:根据返回值大小,临时变量存储的方式也不同 小:寄存器 大:main中提前开一块空间进
行存储临时变量)
这种我们称之为传值返回.
不同于传值返回,传引用返回,是将别名传回来,不需要创建临时变量.
因此我们也可以猜测到,传引用返回的变量,必须在函数销毁的时候,依旧存在,假如销毁了,那
传别名没有意义!
下面这段代码,就很好的诠释了这点.
#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
由于c变量在函数栈帧销毁的同时,也一起被销毁,所以返回出的结果是未定义的,而并不是3.
传引用返回优势:
1.减少拷贝
2.调用者可以修改返回参数
如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给
系统了,则必须使用传值返回.
5.指针和引用的区别
1. 引用概念上定义一个变量的别名,指针存储一个变量地址.
(引用的底层实现,依旧是指针)
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体(引用不可以修改,而指针可以)
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
补充:两者的权限都可以缩小,但不能放大
const修饰的变量是不可以被修改值的,假如用指针或者引用,都需要const修饰,否则程序会报错.
#include <iostream>
using namespace std;
int main()
{
const int a = 2;
const int* pa = &a;
const int& b = a;
//Error
const int a = 2;
int* ppa = &a;
int& b = a;
return 0;
}
六.内联函数
简单编写一个Add的宏函数,相信很多初学者都会写错.
#define ADD ((x) + (y))
这也解释了宏的缺陷,容易写错,而且容易造成代码冗余,并且没得调试!
祖师爷于是引入了inline关键字,彻底解决宏函数问题.
在函数名前面加上inline,编译器在编译期间,会将我们的小程序用函数体直接替换,不再需要函
数调用,起到空间换时间的目的.
为什么说是空间换时间呢?
假如设计了一个swap函数,总共3行代码,在主函数中被调用了1000次.
如果没有inline,那单纯来看,由于被调用的是同一个函数,所以总的代码只有1003行.
如果引入inline,所有位置都会被替换为函数体,这时候代码就是3000行.
注意:
1.inline对于编译器而言只是一个建议,假如函数种存在递归,则还是会直接开辟函数栈帧调用.不同编译器关于inline实现机制可能不同
2.inline不建议声明和定义分离,分离会导致链接错误.因为inline被展开,就没有函数地址,无法实现链接.
3.支持在同一个项目中的不同源文件定义函数名相同但实现不同的inline函数,因为inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突
七.auto(C++11)
随着程序越来越复杂,类型也会越来越复杂.
为了使程序变简洁,C++11引入auto关键字,可以自动识别变量类型.
#include <iostream>
using namespace std;
int main()
{
int a = 10;
auto pa = &a; //自动推导出b的类型为int*
auto* ppa = &a; //自动推导出c的类型为int*
auto& ap = a; //自动推导出d的类型为int
//打印变量b,c,d的类型
cout << typeid(pa).name() << endl;//打印结果为int*
cout << typeid(ppa).name() << endl;//打印结果为int*
cout << typeid(ap).name() << endl;//打印结果为int
return 0;
}
PS:auto不可用于识别数组类型.
八.基于范围的for循环(C++11)
借鉴其它语言的遍历数组,C++引入了基于范围的遍历
这是我们以前遍历数组的方式,利用sizeof来计算数组元素个数.
#include <iostream>
using namespace std;
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
cout << array[i] << " ";
cout << endl;
}
int main()
{
TestFor();
return 0;
}
结合我们上面学习到的auto关键字,可以修改为下面的程序
for循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量
第二部分则表示被迭代的范围
其余和之前循环方式一样,可加break,可加continue等等
#include <iostream>
using namespace std;
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
//为了对原数组数据进行修改,要补上&
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
cout << endl;
}
int main()
{
TestFor();
return 0;
}
九.指针空值nullptr(C++11)
下面看这段程序,按照我们的预想,传NULL指针的时候,应该输出Fun(int *)
#include <iostream>
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); //打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
return 0;
}
但是却是调用第一个Fun函数,这是为什么呢?
我们可以转到NULL的定义去看,如果__cplusplus被定义,NULL指针就等于0
因此,在c++眼里,NULL和0是没有区别的.
所以,我们还需要引入空指针nullptr,对应原来C中的NULL指针.
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的.
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr