C++1:关于C++的基础概念

C++,作为C语言的升级优化版本,它有了更多比较复杂的概念和特化的东西,其中的细节也变得及其繁多复杂,比如类和对象,所以在学习更复杂的东西前,了解概念是绝对不会错的。

目录

命名空间

命名空间的细节:

关于std的两个问题:

1.std到底是啥?

 2.为什么上来就要using namespace std 呢?

缺省参数

1.什么是缺省值?怎么使用缺省值?

 2.缺省参数的类型

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

4. 缺省值必须是常量或者全局变量

函数重载

 重载规则:

 重载原理:

引用

 引用的概念

 引用特性

 1. 引用在定义时必须初始化​编辑

 常引用

使用场景

1. 做参数

2.做返回值

内存空间的销毁意味着什么?

 3.传引用的特点

4.同指针相比,引用有什么不同?

内联函数:inline

auto关键字

指针空值nullptr(C++11)


命名空间

 刚开始接触C++的时候,咱们一般会肌肉记忆般的打上这一串代码

using namspace std;

翻译过来就是使用命名空间std。

那么命名空间究竟是什么?这个std又是啥?我们先看看这个命名空间究竟是什么

命名空间出现的契机:

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存
在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,
以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的

举例:

在C语言下,如上的代码会报错,为什么?

 总的来说,被C库内部所声明过的变量名称在C语言内部是无法被使用的,说人话就是C库里自己用过的变量名你别再想用啦!

如果我偏偏就想用呢?我不希望有东西打破我的命名规则!到了C++,我们就可以用命名空间去规避这个问题了。

 可以看到,我们创建了一个命名空间称之为mynamespace,然后在里面创建了一个rand,赋值为4,在打印的时候,使用了::域作用限定符

域作用限定符::

我们从上图的例子就很容易知道域作用限定符的工作原理,查找指定命名空间内部的变量名称,若是找不到则会报错,没有指定命名空间时,将会在全局进行查找,比如没有携带mynamespace::的rand查找到的就是位于stdlib内部的rand函数的地址。

命名空间的细节:

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

注意:所有命名空间内部的变量都是全局变量,而非局部变量

 2.命名空间可以嵌套


namespace N1
{
	int N1x = 10;
	namespace N2
	{
		int N2x = 20;
	}
}

 但是嵌套的命名空间不能互相访问其空间内的变量

3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个

什么意思呢?看下图就明白了:

 注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

关于std的两个问题:

1.std到底是啥?

2.为什么上来就要using namespace std 呢?

1.std到底是啥?

我们已经了解了命名空间,那么std这玩意其实也是一样的就是一个叫做std的命名空间。

std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中,而Std这个命名空间则包含在<iostream>这个头文件中

那么cout和cin是个什么角色?为什么能实现打印的功能?
1. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
iostream >头文件中。
2. <<是流插入运算符,>>是流提取运算符。
3. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型

 所以,我们打印的时候,cout前面要有个std这个问题就迎刃而解了

 2.为什么上来就要using namespace std 呢?

看起来cout是非常常用的运算符,但是总是要在前面包一个std::也太挫了,怎么省去这个步骤呢?

using namespace std展开,标准库就全部暴露出来了,相当于将std前面的namespace去掉,里面所有的运算符都会变成全局变量,这样子就很方便了,我们也可以像最开始打印hello world!一样非常方便的使用这个运算符。所以using namespace std本质是一个展开的过程。

但是将命名空间展开就会回到最初那个头疼的问题,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。不过日常使用还是没啥问题的。

当然cout用起来也非常舒服,不需要像C语言一样还需要指定打印的数据类型,直接打印即可

缺省参数

1.什么是缺省值?怎么使用缺省值?

C++在调定义函数时,可以指定一个缺省值

int Add(int x = 1, int y = 1)
{

	return x + y;
}

 假设我们不传递参数,就会默认使用给予的缺省值。

 如果我们传递参数,那么以传递的参数为先

 2.缺省参数的类型

全缺省参数(所有参数给与缺省值)

int Add(int x = 1, int y = 1,int z = 1)
{

	return x + y + z ;
}

半缺省参数(部分参数给予缺省值)
半缺省参数必须从右往左依次来给出,不能间隔着给

int Add(int x , int y = 1,int z = 1)
{

	return x + y + z ;
}

 这两种都不行

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


这样会报错,因为编译器无法确定到底用哪个缺省值

4. 缺省值必须是常量或者全局变量


缺省值可以优化初始值,比如栈结构的初始化, 缺省参数这个名字看上去比较高大上,但其实它的本质可以理解为默认值

函数重载

 重载规则:

函数名相同,参数不同,参数的不同要么是类型,要么是顺序,要么是个数

 以上两个函数构成了重载,虽然它们名字相同对于C++来说,它会自我识别类型,不会冲突

注意:

重载必须是参数列表有所不同(包括个数和类型),所以参数类型不同,构成重载

函数重载不能依靠返回值的不同来构成重载,因为调用时无法根据参数列表确定调用哪个重载函数,故错误。

引用是可以构成重载的,但是!是没有用的。尽管它符合重载的定义。


 重载原理:

那么为什么C++能做到函数重载的功能,而C没有?

C语言生成可执行程序的过程如下图所示

 我们先回顾以下编译链接的特性与规则,我们在实现一个较为大的代码工程的时候,为了方便管理,我们一般会声明与定义分离,在.h文件内声明一个函数的整体,返回值,参数列表等,而其定义则是在.c文件中

那么,编译器为了能成功的将“身首异处”的一个函数成功拼回去,将会在链接阶段去到头文件的.o文件寻找其符号表对应的地址,以实现函数的调用。

而在C++中,这个过程有一个特殊的函数名修饰规则,当某一个函数构成了重载的时候,编译器会依据这个规则,为当前的这个函数修饰一个独有的名称,这个修饰名称的规则在VS下非常复杂,但是在GCC中非常简单易懂,如下图所示:

 Gcc编译后:

简而言之,为了能成功实现函数重载,C++为每个函数都暗地里以自己的规则取了个小名


引用

 引用的概念

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

什么意思呢?比如说我们可以叫菠萝这个水果的别名:凤梨

 虽然说名字不一样,但是我们都知道这两个名称都是指向这种水果

在C++中,引用同理,我们创建一个x变量,为它取个别名叫y,格式如下

int main()
{
	int x = 10;
	int& y = x;

	cout << x << endl;

	cout << y << endl;

	return 0;
}


 引用特性

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

 1. 引用在定义时必须初始化

 2. 一个变量可以有多个引用

 3. 引用一旦引用一个实体,再不能引用其他实体


 常引用

void TestConstRef()
{
    const int a = 10;
    //int& ra = a; // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

使用场景

1. 做参数

int Add(int& x , int& y , int& z )
{

	return x + y + z;
}

2.做返回值

int& Add(int& x , int& y , int& z )
{

	return x  ;
}

 注意,做返回值时,无法返回一个表达式

 引用有一个非常有趣但危险的问题,如下:

//这段代码会输出什么?
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语言最基础的知识告诉我们,这个返回值的N在出了Count的作用域时就已经销毁了,当我们将引用使用到返回值上面的时候又是怎么样的?

这里我们打印出来康康,不仅打印,还对其做修改。

 诶?真是奇怪,难不成空间会在使用引用的时候还能特例保存不成?

为了回答这个问题,我们要重新探讨一下空间销毁到底意味着什么。


内存空间的销毁意味着什么?

1.空间还在吗?

空间依然还在,只是使用权不是我们的了,存入的数据不再被保护

2.还能访问吗?

能访问,但是读写的数据是不确定的。

出了函数作用域,返回变量不存在了,不能引用返回,因为引用返回的结果是不定的。

那么传引用返回的价值体现在哪?

1.可以有效的减少拷贝,对于返回的值来说,如果返回的比较大,那么可以提高效率。

2.可以修改返回值

回到我们最开始的Add程序,下图给出了更加底层更加细节的解释

 注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。


 3.传引用的特点

传值返回不能用引用接收

这是一个比较奇怪的特点,至于为什么,原因如下:传值调用时会产生一个临时变量,这个临时变量具有常性,也就是不能修改

也就是如下的代码产生了权限放大的问题

const int b = 10
int& a = b;

 传值返回不能用引用接收,也是相同的道理,因为此时返回的是返回值的临时拷贝,临时变量具有常性,没法往引用里放


4.同指针相比,引用有什么不同?

我们在语法层面上认为引用的使用是别名不开辟空间,但是在底层逻辑里面,引用变量还是开辟空间的

为什么?我们看一眼汇编语言便知

 我们可以发现,开辟指针,引用的底层汇编语言是一样的,所以引用时使用指针实现的

引用表面好像是传值,其本质也是传地址,只是这个工作是由编译器来做

指针是间接操作对象,引用时对象的别名,对别名的操作就是对真实对象的直接操作,

总结一下:

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


频繁调用的小函数会消耗我们的资源,比如我们在实现霍尔的挖坑法的时候会频繁的因为调用Swap函数而消耗一部分的栈帧,那么有没有什么方法可以节省栈帧?

内联函数:inline

内联函数跟宏类似但优于宏,它不仅和宏一样可以不用建立栈帧,也可以实现计算的功能,相较于宏容易写错,内联函数非常方便。

在函数返回值前面加一个inline就可以了

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

内联函数能节省栈帧的原理:通过直接在编译过程展开,可以免去编译器找寻地址的过程,头文件相同,也类似于宏。

 但是要注意,内联函数在Debug版本下不会展开,因为为了方便调试,也就是在Debug版本下,inline不会生效

当然Debug下底层汇编是看不到的,不过可以调一调让我们看的到。

 内联函数还是有缺点的:

比如不可以拿去替换递归函数,名字太长的也不太适合,太长或者太复杂的函数内联请求会失效。

内联说明只是向编译器发出的一个请求,编译器可以忽略这个请求。


 一些问题:

为什么函数长了会不展开呢?

因为会引起代码膨胀。这里的变大指的是可执行程序的大小变大,而不是栈帧

假如我们强行去展开会怎么样?

内联说明只是向编译器发出的一个请求,编译器可以忽略这个请求。

 inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到

为什么会找不到?

我们正常调用链接函数的过程是先找到函数的声明,然后在链接的时候寻找这个函数的定义,预处理阶段已经将头文件展开合并了,但内联完全不会产生符号表,没有符号表就找不到里面的定义的地址,编译器就不知道怎么实现这个函数体。

而内敛函数一旦声明,它就绝对不i会进符号表,不管它最终是否是内联成功,在这里,编译器看的只是这个函数的属性是不是内联函数,你就算函数很长很长它没有成功展开,它也是不会成功的。


auto关键字

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (auto& e : array)
		e *= 2;
	for (auto e : array)
		cout << e << " ";
	return 0;
}
void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
}

范围for的使用条件

1. for循环迭代的范围必须是确定的

2. 迭代的对象要实现++和==的操作。


指针空值nullptr(C++11)

 在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现
不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下
方式对其进行初始化:

void Text()
{
    int* ptr = NULL;
    int* ptr2 = 0;
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

 这里就衍生了一个问题:为什么到了C++之后不使用NULL而去使用nullptr了?

 可以看到,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,因此与程序的
初衷相悖。

1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
 

 到这,C++的基础入门概念就概述完毕了,细节较多,但是还不算困难,希望对你有点帮助!感谢阅读!

猜你喜欢

转载自blog.csdn.net/m0_53607711/article/details/128417295