前言:本文主要介绍有关C++入门需掌握的基础知识,包括但不限于以下几个方面,这里是文章导图:
本文较长,内容较多,大家可以根据需求跳转到自己感兴趣的部分,希望能对读者有一些帮助
那么本文也主要以导图为思路进行分享,话不多说,让我们开始吧
命名空间的设计主要是为了解决C语言中有关全局域的命名冲突问题,请看下面这个例子:
#include
#include
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
如上代码编译后会报错,因为其中的rand
既是全局变量的变量名,又是头文件stdlib.h
中所包含的函数名。二者都处于全局域当中,由此就造成了命名冲突。
那么通过命名空间的使用,我们就可以实现对标识符的名称进行本地化,
以避免命名冲突或名字污染。
命名空间中可以定义变量、函数和类型,但需注意的是,命名空间仅完成定义的操作,不能具体执行语句和调用函数,真正执行语句和调用函数的地方还是在主函数当中。命名空间的定义借助C++的关键字namespace
来实现,其基本的定义形式为:
namespace 空间名
{
命名空间的成员内容
}
由此大体可以将其划分为三种定义形式:
namespace Tardis
{
int rand = 10;
int Sub(int a, int b)
{
return a - b;
}
struct s1
{
int _s;
};
}
namespace Tardis
{
int rand = 10;
namespace Tardis1 //Tardis中嵌套Tardis1
{
int Sub(int a, int b)
{
return a - b;
}
struct s1
{
int _s;
};
}
}
同一个工程允许存在多个相同名称的命名空间,编译器最后会将具有相同空间名的命名空间定义合成同一个命名空间。所以需要注意的是,在同一个命名空间中仍需避免相同变量名的出现,否则就会出现重定义问题。
以这段代码作为例子:
namespace Tardis
{
int a = 10;
int Sub(int a, int b)
{
return a - b;
}
struct s1
{
int _s;
};
}
::
int main()
{
cout << Tardis::a << endl;
cout << Tardis::Sub(10,5) << endl;
Tardis::s1 stest;
}
using Tardis::a; //引入变量a;
using Tardis::Sub; //引入函数Sub;
using Tardis::s1; //引入结构体s1;
int main()
{
cout << a << endl;
cout << Sub(10,5) << endl;
s1 stest;
}
using namespace Tardis;
int main()
{
cout << a << endl;
cout << Sub(10,5) << endl;
s1 stest;
}
这里对域作用限定(访问)符补充一点:域作用限定(访问)符本质用来告诉编译器在编译阶段是否到该限定符的左侧的域(空白代表全局域)去进行访问/搜索,编译器在默认情况下是不会去搜索命名空间的。由此就会产生访问顺序的问题,如下面这段代码:
int a = 5; //全局
namespace Tardis
{
int a = 10; //命名空间
}
int main()
{
int a = 1; //局部
cout << a << endl;
cout << ::a << endl;
cout << Tardis::a << endl;
}
运行结果:
从运行结果可以看出,第一次输出的时局部域中的a,第二次则通过域访问限定符输出了全局的a,第三次同理输出了命名空间中的a。
所以变量的搜顺序可以总结为:
局部域 --> 全局域-->展开了的或指定访问的命名空间域
最后还有一个展开命名空间的问题,如下:
由于展开了命名空间就相当于把命名空间中的内容暴露到了全局,所以编译器在编译时无法确定该使用哪个变量。
结论:在正式的工程项目中最好不要全展开,可以只引入某些个成员。在日常的代码练习中,可以全部展开。
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
在一些函数功能的实现中,对于某些变量,我们有时候希望它是一个具体的值,有时候又希望它是一个固定或者说默认的值。比如有这样一个申请空间的函数:
int* Apply(int n, int init_val = 0);
变量n
表示需申请的整型空间的个数,而变量init_val
表示对所申请到的空间内容所进行初始化的值,默认为0。这样就可以根据不同的需求来申请空间:
Apply(5); //申请5个整型空间
Apply(5,1); //申请5个整型空间并将空间内容初始化为1
上面仅是一个不太合适的例子,缺省参数的伟大之处在往后的C++学习中才会不断体现,同时也需要我们去不断感受。
缺省参数的形式分为全缺省和半缺省两种,像上面的例子就属于半缺省。具体还可以看下下面这两个:
//全缺省
void func(int a = 10, int b = 20, int c = 30)
//半缺省
void func(int a, int b = 20, int c = 30)
代码例:
//全缺省:
void func(int a = 10, int b = 20, int c = 30)
func(1); //1传给a
func(1,2); //1传给a;2传给b;
func(1,2,3); //1,2,3分别传给a,b,c
//半缺省:
void func(int a, int b = 20, int c = 30) //正确形式
void func(int a = 10, int b, int c = 30) //错误形式
可以看出,这两条规则是同时作用,不可分割的。
C++中,函数除了缺省参数,另一伟大功能就是函数重载了。
在C语言中,如果我们需要实现两种不同类型(不包括隐式类型转换)的加法可能就只能这样实现:
int Addi(int a, int b); //实现整型的加法
float Addf(float a, float b); //实现浮点型的加法
当类型较多时,我们也就需要更多不同名称的函数去匹配。这样一来多少会让整体的代码失去一些可读性。函数重载就能很好地解决这个问题及其他一些相关问题。
C++中允许在同一作用域中声明几个功能类似的同名函数,但同名函数的声明需满足以下三个规则之一才能构成正确的函数重载
那么对于上面的加法函数,可通过规则1设计为:
//参数类型不同
int Add(int a, int b);
float Add(float a, float b);
需要注意的一点:全缺省的函数和无参的函数不能同时出现
因为二者虽然根据规则能构成重载,但在调用时会出现歧义(调用不明确):
void func(int a = 1)
{
cout << a << endl;
}
void func()
{
}
int main()
{
func(); //编译器报错: error C2668: “func”: 对重载函数的调用不明确
return 0;
}
其实和C语言相似,但是重载多了一条规则——名字修饰规则。
我们知道,程序的运行需经历这么几个过程:预处理、编译、汇编、链接。函数重载实现的阶段就在最后链接的阶段。下面通过Add这个函数作为例子进行简单说明:
假设Add.h中写函数声明,Add.cpp中写函数的定义,Test.cpp中写主函数
那么需要在Test.cpp中包含头文件Add.h后,才能调用Add函数;
程序运行后,先进入预处理阶段,进行头文件展开、宏替换等工作;
接着进入编译阶段,生成汇编代码;
接着进入汇编阶段,对cpp文件中的函数生成各自的符号表;
最后在链接时,编译器才会将各文件的符号表合并在一起,从而正确找到那个调用的函数。
那么对于构成函数重载的函数,根据名字修饰规则,它们会拥有不同的符号,这些符号又对应一个不同的地址,从而让编译器在调用函数时,能够正确跳转到需要调用的那个函数。
不同的平台下名字修饰的规则可能大相径庭。关于不同平台的名字修饰规则感兴趣的读者可以自行了解一下,本文这里就不做过多说明了。
引用是给已有变量取的一个别名,其和被引用的对象共用同一块内存空间。简单理解就是:有一个叫a的人被起绰号(被引用)为b,那么当通知a或通知b时,其实是通知的都是同一个人,因为b只是a的一个别名(绰号)。a就相当于身份证上的名字,b就相当于好朋友对你起的绰号。
如上体现在代码中就为:
int a = 1; //a本身
int& b = a; //a被起绰号为b
cout << a << " " << b << endl;
a++;
cout << a << " " << b << endl;
掌握常量引用仅需记住一条有关“权限”的原则:引用过程中,权限不能放大。请看例子:
const int a = 1;
int& ra = a;
//错误,因为a为常量不能改变,而int&的引用类型可以改变其引用的对象
//权限放大
const int a = 1;
const int& ra = a;
//正确
//权限平移
int a = 1;
const int& ra = a;
//正确,const int&的引用类型只是限制了作为a的别名ra的权限,a仍可通过自身改变
//权限缩小
易混淆点:
const int a = 1;
int b = a;
//正确,因为b和a使用的并不是同一块空间,这里仅将a的值拷贝给b,改变b并不影响a
引用做参数主要适用于下面两个场景:
例:
//原来写swap函数
swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//通过引用写
swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
引用返回,也就是给返回的变量起了别别名然后再返回。传引用返回与引用做参数的使用场景类似,都能提高效率,但传引用返回在使用时会有一些限制,请看下面的代码:
//正确的传引用返回
int& func()
{
static int n = 1;
n++;
return n;
}
//错误的传引用返回
int& func()
{
int n = 1;
n++;
return n;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
解释:
了解了限制条件后,我们再来看看其提高效率的本质,其实和引用作为参数提高效率的本质相同。
传引用返回提高效率的本质:
需要先理解传值返回的本质,对下面这段代码:
int func()
{
int n = 1;
n++;
return n;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
main函数中的变量ret中的内容并不是直接变为func函数所返回的值,而是需要经过一个临时变量对其进行内容更改。这个临时变量一般是一个寄存器。示意图如下:
也就是说,值返回会先将返回对象拷贝给临时变量,再由临时变量拷贝给接收返回值的变量。这样一来,如果是一个大对象或需要深拷贝的对象,那么传值返回时进行的拷贝就会占用大量开销。而传引用返回就不会有这样的问题,因为传引用返回相当于是给返回的变量起了个别名再返回,本质上还是返回那个变量本身,所以不会产生临时变量,进而提高了效率。
总结:
上面说到函数在进行值返回时本质会生成一个临时变量,需要注意的是,这个临时变量是具有常性的。请看下面这段代码:
int func()
{
int n = 1;
n++;
return n;
}
int main()
{
int& ret = func(); //编译器报错:error C2440: “初始化”: 无法从“int”转换为“int &”
cout << ret << endl;
return 0;
}
从原理的角度分析:func函数返回时产生了一个具有常性的临时变量,而接收类型却是int&的引用类型,权限被放大了。所以正确写法应该是:const int& ret = func();
如上会产生临时变量的还有一个场景:隐式类型转换
请看下面这段代码:
double b = 1.1;
int& a = b; //编译器报错:error C2440: “初始化”: 无法从“double”转换为“int &”
由前面对引用的特性我们知道,引用类型和引用实体必须是同类型的,但对于一些相互之间可以进行隐式类型转换的类型,引用其实也支持。上面这段代码报错关键还是在于权限的放大,因为b
会先产生一个int
类型的临时变量,然后再进行内容的拷贝。示意图:
又因为临时变量具有常性,所以直接使用int&
的引用类型是将权限放大了。正确写法应该是:const int& a = b
引用底层虽按指针方式实现,但在上层也就是实际使用时,在语义层面上是和指针完全不同的。
请看下面示意图:
从语义上来说,指针变量是开了空间的,存储变量的地址;引用则是变量的别名,就是变量本身。
也可理解为:指针变量会有一个解引用而去“找”的动作,但引用就是变量本身。
通过前面两点主要想表明引用在上层使用时的长处,接下来从各方面总结一下两者的不同
- 引用在定义时必须初始化,而指针不必;
- 引用初始化后引用的对象不能改变,而指针可以在同类型的前提下在任何时候改变指向的对象;
- 没有空引用,但有空指针;
- 没有多级引用,但有多级指针;
- 引用在语义层面是变量的一个别名,而指针存储变量的地址;
- 通过sizeof计算时的结果不同:引用的大小即为被引用类型的大小,而指针的大小永远时4或8个字节;
- 执行自增的结果不同:引用执行自增时相应被引用变量自增1;而指针执行自增时指针向后偏移一个类型的大小;
- 访问实体方式不同:对引用编译器会自己处理;而指针需显示解引用;
- 引用在使用时相对指针更方便也更安全;
以关键字inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方直接展开,而没有函数调用建立栈帧的开销,从而提升程序运行的效率
对于宏函数,其优点是:不需要建立栈帧,提高调用效率,易于修改;缺点是:形式复杂,容易出错,不利于调试;
内联函数则较好地沿袭了宏函数优点的同时,又避开了宏函数的缺点。根据特性,一般建议将规模较小、不是递归、且频繁调用的函数声明为内联函数。
前文其实已经说到,内联底层其实就是将调用的函数在调用处直接展开。下面请看验证示例:
示例代码:
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 2);
}
没有将Add函数设置内联的汇编:
将Add函数设置为内联后的汇编:
auto在C中用来表示具有自动存储器的局部变量;在C++11中,auto作为新的类型指示符来指示编译器推导类型。
auto的类型推导作用主要用来应对一些较长的类型从而简化代码,方便开发。如在将来的学习中可能有遇到这样的类型:
std::map<std::string, std::string> m;
std::map<std::string, std::string>::iterator it = m.begin();
//如上第二条语句利用auto推导可写为:
auto it = m.begin();
关于auto的使用规则如下:
int x = 1;
auto a = &x; //auto推导为int*
auto* a = &x; //auto推导为int
auto& a = x; //auto推导为int
auto的实现是在编译阶段。在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。可以这么理解,auto并非是某一种具体“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
前文说到,关键字auto一般配合范围for进行使用,请看代码例:
int arr[]={1,23,45,67};
//一般迭代遍历数组
for(int i = 0; i < (sizeof(arr)/sizeof(arr[0]); ++i)
{
cout << arr[i] << " ";
}
//使用范围for迭代遍历数组
for(auto e: arr)
{
cout << e << " ";
}
//冒号“ :”前面是范围内用于迭代的变量,类型一般用auto,也可手动指定
//后面表示被迭代的范围,一般是需要遍历的对象名
这里主要和C语义中的宏定义NULL
进行区别,C++11中,nullptr
作为关键字表示指针空值。用sizeof
对二者进行计算的结果相同,C++开发中一般推荐使用nullptr
表示指针空值从而提高代码的健壮性。
本章完。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还请过路的朋友们留个评论,多多指点,谢谢朋友们!