【C++】C++入门

C++在C语言的基础上增加了命名空间以解决命名冲突问题,缺省参数允许函数参数设定默认值,函数重载则允许同名函数根据参数列表不同进行区分。引用作为变量的别名,不占用额外内存。auto关键字用于自动推导变量类型,简化代码。基于范围的for循环简化了集合遍历。内联函数优化了函数调用的性能,nullptr提供了安全的空指针表示。
摘要由CSDN通过智能技术生成

     C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。所以C++需要对C语言存在的不足进行优化,那么这篇文章就介绍几个C++在C语言不足之处做出的改进,一起来看一下吧。

命名空间

了解命名空间之前我们先看一段C语言代码:

这里我们定义了一个全局变量rand,并将其输出,没有问题,但是如果我再加一个头文件,如下:

 他就发生报错了,我们看一下报错原因:

 这里rand重定义的意思其实就是我们变量名和库命名的函数冲突了,我们还有一种冲突情况是项目内部冲突,例如一个团队写一个大型项目,这就需要很多人分工负责不同模块,假如团队成员张三写了一个my_function这样的函数,而李四也写了一个同名函数,那么最终项目不同模块合并时就会出现重名函数的问题。

这种命名冲突是C语言解决不了的问题,因此C++中提出了命名空间的概念。

(这里C++可以将rand存入一个指定的域内,然后通过域进行访问)

命名空间可以定义变量/函数/类型

那么命名空间是如何解决上述问题的呢?

要想使加了<stdlib.h>这个头文件之后代码仍然正确,我们就需要把rand这个变量放在一个命名空间域内,这个域就起到一个隔离的作用。

定义命名空间,需要使用到 namespace 关键字 ,后面跟 命名空间的名字 ,然 接一对 {} 即可, {}
中即为命名空间的成员,{}代表的就是域。
不同的域内可以存在同名现象

我们举个例子:

 这里的两个a可以同时存在,因为它们属于不同的域,第一个a在全局域,第二个a在局部域,但是如果我们用printf输出a是优先局部域的,要想输出全局域中的a,就需要使用域作用限定符::,如果这个限定符前面不加东西代表的就是全局域:

 一般存在同名变量,局部域优先,局部域没有,就去全局域,但是如果全局域没有,要想访问命名空间域内的变量就需要域作用限定符::指定或者展开命名空间域,否则会报错:未定义标识符。

 

如果这里展开命名空间,并且存在全局变量,那么会优先访问哪个呢?

答案是会报错:

 因为展开命名空间域就意味着将其暴露在全局,因此会和全局的同名变量冲突,所以慎重考虑是否选择using namespace,最好使用时用域作用限定符进行访问

 我们经常看到C++代码中有“using namespace std”这里的含义就是展开std命名空间,std就是标准库的一个命名空间。

命名空间是可以嵌套

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;
         }
     }
}

同一个工程中允许存在多个相同名称的命名空间,编译器最后会合并成同一个命名空间,当然这些同名的命名空间内不能存在同名的变量/函数/类型,因为合并时会报错。

在一个项目中,我们通常不会使用using namespace std,因为这样存在风险,假设你定义了多个变量名与库内存在重名,那么当某天包含库中的某个头文件,可能会出现很多重命名的报错,所以我们习惯性的要使用域作用限定符进行指定。但是这样依然存在一个小问题,假设一条语句使用十分频繁,那么一直使用域作用限定,会十分繁琐,那么我们可以进行如下操作:使用using std::XXX展开部分 

缺省参数

缺省参数是声明或定义函数时为函数参数指定一个缺省值。在调用该函数时,如果没有指定实参时则采用该形参的缺省值,否则使用指定的实参。

 如果函数有多个参数,均有缺省值,传参时只给定部分参数,那么会按从左到右的顺序传参:

 不可以跳跃传参,即不能只给定a,c参数,不传b的参数,只能依次传参。

缺省参数分为全缺省参数和半缺省参数,即都给定缺省值和给定部分缺省值,但是半缺省参数有要求:半缺省参数必须从右往左依次来给出,不能间隔着给

缺省参数有一个十分实用灵活的场景,比如我们数据结构中的栈的初始化,当我们未知需要多少空间时,选择的方法是空间不够自动扩容,但如果我们已知需要的空间大小,依然这样不断判断加扩容会十分繁琐,这时候缺省参数就有意义了:传参时再加一个参数用来指定大小,给一个缺省参数4,这样已知空间大小可以直接开辟指定大小,未知空间大小可以使用缺省参数开辟一小部分空间,空间不够再进行扩容,十分灵活。

tips:缺省参数不能在函数声明和定义中同时出现

缺省参数在声明时给出

因为如果只在定义时给的话,编译时会报错(报错原因其实就是编译过程中会展开头文件,但是两个cpp文件单独进行,也就是test.cpp看不到另一个cpp文件中所写函数的内容,只能看到该函数的声明,由于声明没有给缺省,所以编译器不接受你不传参数)

函数重载

函数重载的概念:函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似同名函数,这些同名函数形参列表(参数个数或类型或类型顺序)不同(返回值不做要求),常用来处理实现功能类似数据类型不同的问题。(C语言不允许同名函数)

 接下来解决几个问题:

为什么C语言不支持而C++支持重载?C++又是怎么支持重载的呢?

 

要解决这些问题,我们以上图代码为例先回顾编译链接有哪些过程:

1.预处理:展开头文件Stack.h/宏替换/条件编译/去掉注释,生成Stack.i,Test.i文件;

2.编译:检查语法,生成汇编代码,生成Stack.s,Test.s文件;

3.汇编:将汇编代码转换成二进制机器码,生成Stack.o,Test.o文件;

4.链接:生成可执行程序:xxx.exe / a.out

我们转入汇编代码可以发现调用函数实际就是执行call指令

 我们执行call指令,会跳转到call后面所给的地址,然后执行jmp指令,跳转到函数所在的地址,调用函数,如下图:

 但是结合上面编译链接的过程可知,编译过程中,Test.i->Test.s执行call指令时没有我们所看到的地址(尽管此时展开头文件之后存在函数声明),因为此时的函数地址都在Stack.i文件中,二者并没有交互联系,所以Test.i拿不到对应的地址,但是编译可以通过(存在声明,所以这里函数地址暂时空缺,直到链接之后,填补函数地址)。所以关键之处就在于链接时是否能将所需的函数地址提供到对应的位置,这也是C语言和C++是否支持函数重载的关键之处。

了解了大致过程我们就可以直接说出结论了,为什么C语言不支持函数重载而C++支持函数重载,就是因为链接时编译器为了能够找到对应函数的地址有一套专门的函数名修饰规则,而C语言修饰后名字不变,仍然是原函数名,拿到的地址直接就是函数名的地址,它不管你的参数列表是否一致,只看函数名,所以当你出现同名函数,它就会报错,因为它分不清哪个是所要的地址。

而C++支持函数重载是因为C++在链接时函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,因此同名函数经过函数名修饰处理后,得到的名字不同,根据修饰后的函数名可以找到对应的函数。

Tips:

如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分,产生歧义。
它不同于上面可以利用参数列表进行区分,它最关键的问题在于编译时就无法区分调用的是哪个函数,所以会编译报错。

引用

概念:引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量
开辟内存空间,它和它引用的变量共用同一块内存空间。

结构:类型& 引用变量名(对象名) = 引用实体;

引用类型 必须和引用 实体 同种类型

 引用具有一些特性

1.引用在 定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体

使用场景:

1.做输出型参数(形参的改变不改变实参)

还有一个经典的形参变化不改变实参的例子就是单链表尾插使用的二级指针,有了引用就方便很多也更好理解了:

 引用做参数可以提高效率(针对大对象和深拷贝类对象),以大对象为例:

以值作为参数,在传参期间,函数不会直接传递实参,而是传递实参的一份临时拷贝,因此用值作为参数,效率是非常低下的,尤其是当参数非常大时,效率就更低
2.做返回值
跟将值作为参数过程一样,在返回期间,函数不会将变量本身直接返回,而是返回变量的一份临时的拷贝(因为函数调用结束后 栈帧销毁 ,所以需要临时拷贝保存返回值,这里临时拷贝的行为不会因为你的变量是否会随栈帧销毁而销毁而是否执行拷贝,是否拷贝生成临时变量取决于返回值,引用做返回值就不需要临时变量了),因此用值作为返回值类型,效率低下,尤其是当返回值类型非常大时,效率更低,因此引入引用返回可以避免拷贝,提高效率。
以返回大对象为例进行测试:

 引用返回还有需要注意的地方是,不是所有值都能进行引用返回,比如函数的局部变量作为引用值返回就是错误的,越界访问,因为函数栈帧销毁,局部变量已经归还使用权,再利用别名进行访问,得到的结果是不确定的,如果栈帧销毁没有清理栈帧,那么结果是正确的,但如果清理了栈帧,得到的结果就是随机值。

因此使用引用返回时首先需要判断是否可以使用引用做返回值。

引用过程中,权限不能放大,可以缩小。

接下来看一段代码理解权限不能放大只能缩小:

 我们分析一下这里报错原因是什么意思,无法从const int转换为int&,那么哪里来的const int?

这是因为func1是值返回,传值返回会产生一个临时变量,这个临时变量具有常性,不可被更改,也就是类似于有const修饰,所以要想通过,必须在int&前加const,否则就是权限放大。如果ret1是int则变成拷贝了,就不是权限的问题了。

func2是引用返回,所以可以用int&接收,属于权限的平移,当然加const也可以,是权限的缩小。

引用指针的不同点 :
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用 在定义时 必须初始化 ,指针没有要求。
3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体。
4. 没有 NULL 引用 ,但有 NULL 指针。
5. sizeof 中含义不同 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32位平台下占4 个字节 )
6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小。
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全

auto关键字

auto的特点是自动推导类型,之所以给出auto这个关键字是因为在某些场景下类型名十分冗长复杂,使用typedef有时又容易含义不明确导致出错。

auto 声明的变量必须由编译器在编译时期推导而得
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型 。因此 auto 并非是一种 类型 的声明,而是一个类型声明时的 占位符 ,编译器在编 译期会将 auto 替换为变量实际的类型

auto使用规则:

1. auto 与指针和引用结合起来使用
auto 声明指针类型时,用 auto auto* 没有任何区别,但 auto 声明引用类型时则必须 &
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

auto不能使用的场景:

1. auto不能作为函数的参数( auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 )
2. auto 不能直接用来声明数组

基于范围的for循环

在范围for出现之前,C++遍历数组是这样遍历的:

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* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
		cout << *p << endl;
}

但是对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候容易犯错误。因此C++11中引入了基于范围的for循环。

void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
     e *= 2;
for(auto e : array)
     cout << e << " ";
return 0;
}
for 循环后的括号由冒号 分为两部分:
第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围。
范围for的使用条件:
1.for 循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供begin和 end 的方法, begin end 就是 for 循环迭代的范围。
2.迭代的对象要实现 ++ == 的操作

内联函数

我们在调用一个函数时是有很多开销的,比如函数栈帧的建立,变量的创建等一系列消耗,假设我们有一个结构简单的函数会被频繁调用,那么函数栈帧就会被频繁的建立,开销太大,这是一个需要优化的地方,C语言中优化方式是使用宏替换

int Add(int a, int b)
{
	return a + b;
}

//宏替换
#define ADD(x,y) ((x)+(y)) 

int main()
{
	for (int i = 0; i < 10000; i++)
	{
		cout << Add(i, i + 1) << endl;
		cout << ADD(i, i + 1) << endl;
	}
	return 0;
}

虽然不需要建立栈帧,但是宏替换有很多缺点:

1. 不方便调试(因为预编译阶段进行了替换)
2. 导致代码可读性差,可维护性差,容易出错
3. 没有类型安全的检查 

那么C++中是如何优化的呢?答案就是内联函数。

内联函数格式很简单,就是在函数前用inline修饰,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销。

没有使用内联函数,我们看汇编代码可以看到call Add指令

 使用内联之后:

内联函数的特性:
1. inline 是一种 以空间换时间 的做法,如果编译器将函数当成内联函数处理,在 编译阶段,会
用函数体替换函数调用 ,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2. inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同 ,一般建议:将函数规模较小 ( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现 )
是递归、且频繁调用 的函数采用 inline 修饰,否则编译器会忽略 inline 特性。
3.inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址
了,链接就会找不到。

nullptr

我们在声明一个变量时最好会给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int* p1 = NULL ;
int* p2 = 0 ;
NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif
可以看到, NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何
种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如如下代码:
void f(int)
{
 cout<<"f(int)"<<endl;
}
void f(int*)
{
 cout<<"f(int*)"<<endl;
}
int main()
{
 f(0);
 f(NULL);
 f((int*)NULL);
 return 0;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0 ,因此与程序的
初衷相悖。
C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
Tips:
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入
2. C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值