24 初识C++

news2024/9/20 19:50:03

目录

一、C++概念

(一)C++是什么

(二)C++的历史

(三)C++参考文档

二、第一个C++程序

三、C++的域

四、命名空间 namespace

(一)namespace 的作用

(二)namespace 的定义

(三)namespace 的使用

1、使用【域作用限定符::】对命名空间内容进行引用

2、使用 using 展开整个命名空间

3、使用 using 展开命名空间中的某变量/函数/结构

五、C++的输入与输出

(一)标准输入与输出

(二)流运算符

(三)换行

(四)std::cout 和 std::cin 与 printf 和 scanf 的对比

六、缺省参数

七、函数重载

八、引用 

(一)引用的概念和定义 

1、概念

2、定义

3、引用 与 typedef 和 define 的区别

(二)引用的特性

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

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

3、引用过一个实体后,不能改变指向

(三)引用的使用

(四)const引用

1、const引用与const对象

2、临时对象触发权限放大

3、const引用的使用

(五)指针和引用的关系

九、inline 内联函数

十、nullptr

十一、有意思的点

(一)越界访问不一定报错

(二)引用与指针的底层


一、C++概念

(一)C++是什么

        C++本意为祖师爷本贾尼不满足C语言所存在的缺陷而做的拓展。

(二)C++的历史

        其中大字体的为大版本更新,小的为小版本。

(三)C++参考文档

        https://legacy.cplusplus.com/reference/

        https://zh.cppreference.com/w/cpp

        https://en.cppreference.com/w/

        说明:

        第一个链接不是C++官方文档,标准也只更新到C++11,但是以头文件形式呈现,内容比较易看好懂;

        后两个链接分别是C++官方文档的中文版和英文版,信息很全,更新到了最新的C++标准,但是相比第一个不那么易看;

        几个文档各有优势,需要结合着使用。

二、第一个C++程序

#include<iostream>
using namespace std;
int main()
{
	cout << "Hello World" << endl;
	return 0;
}

        结果如下所示:

         若要看懂上面的代码,需要先学习几个知识点。

三、C++的域

        C++中的域有函数局部域,全局域,命名空间域,类域。

        域的影响:

                ① 域做到了名字隔离,不同域可以定义同名变量;

                ② 影响生命周期:

                全局域中的变量/函数的生命周期是随着程序结束而结束的,而函数局部域中的变量/函数的生命周期则是出了该作用域就进行销毁;

                命名空间域和类域不影响变量的生命周期。 

                ③ 影响编译查找逻辑:使用变量/函数的时候,如果不限定域名,则会先使用局部域,再搜索全局域。

四、命名空间 namespace

(一)namespace 的作用

        在C/C++中,有众多的变量、函数和类都存在于全局作用域中,而这些变量、函数和类的名字很有可能会与包含的头文件中的变量、函数和类的名字起冲突,因为同一作用域中不能有相同名字的定义;

        使用命名空间的目的是对标识的名称进行本地化,以避免命名冲突或者名字污,namespace关键字的出现就是针对这一问题的。

(二)namespace 的定义

        namespace只能定义在全局域中,其中的变量、函数、类型也属于全局域的

        命名空间 namespace 的定义类似于结构体的定义,使用方法如下:

        如下代码演示:

int i = 10;
namespace zyb
{
	int i = 100;
}

        namespace 本质上是创建了一个域来解决问题,因为不同域可以定义相同名字的变量/函数/类型,如上代码的变量 i 就不存在命名冲突的问题。

        注意:

                ① 在工作中一般使用项目名称作为命名空间名;

                ② 只能定义在全局域中,因为本身就是全局的;

                ③ namespace 能嵌套定义,目的是为了解决命名空间中的同名问题,如下代码所示:

namespace zyb
{
	namespace a
	{
		int i = 10;
	}

	namespace a
	{
		struct i
		{
			int num1;
			double num2;
		};
	}
}

                ④ 命名空间域和全局域的生命周期一样,都是随着程序结束而结束。

                ⑥ 多文件使用同名的命名空间域不会冲突,系统会把同名的命名空间合并在一起。

        C++标准库已经放在std(standard)的命名空间中,在使用C++标准输入输出时需要使用作用域限定符来指定函数的使用。

(三)namespace 的使用

        想要使用命名空间域中的变量/函数/类型时,不能直接写他们的变量/函数/类型的名,这样机器在编译查找时会默认先在函数局部域中搜索,再查找全局域中是否存在想要使用的变量/函数/类型,并不会主动去命名空间域中查找,所以需要指定使用命名空间域中的变量/函数/类型。

        命名空间的使用有三种方法:

1、使用【域作用限定符::】对命名空间内容进行引用

        【域作用限定符 ::】,为两个冒号。

        注意:

                ① 使用命名空间域的变量/函数:命名空间名::变量/函数名

                使用命名空间中的结构体:struct 命名空间名::结构体名,如下示例:

namespace zyb
{
	namespace a
	{
		int i = 10;
	}

	namespace a
	{
		struct i
		{
			int num1;
			double num2;
		};
	}
}

int main()
{
	zyb::a::i = 20;//命名空间名::变量名
	struct zyb::a::i s1 = {s1.num1 = 10, s1.num2 = 1.2f};
    //struct 命名空间名::结构体名
}

                总结:只在名字前面写明【命名空间名::】

                ②若【域作用限定符 :: 】的左边什么都没写,就默认是全局域;

                ③在项目中推荐使用这种方法。

2、使用 using 展开整个命名空间

        在全局域中使用 using 展开命名空间的全部内容

        需要大量使用命名空间中的变量/函数/类型,且没有命名冲突的时候使用,因为展开命名空间相当于把命名空间中的内容暴露在全局域中,此时命名空间域的内容变成了全局域的内容,机器在编译查找时就会找到该变量/函数/类型。

        使用方法如下:

        如下代码所示:

namespace zyb
{
	namespace a
	{
		int i = 10;
	}

	namespace a
	{
		struct ii
		{
			int num1;
			double num2;
		};
	}
}
using namespace zyb::a;
int main()
{
	i = 20;//命名空间名::变量名
	ii s1 = {s1.num1 = 10, s1.num2 = 1.2f};//struct 命名空间名::结构体名
}

        使用特点: 使用方便,但有命名冲突的风险。

        注意:

                ① 展开命名空间与展开头文件是不同的东西,展开头文件是在预处理阶段把头文件的内容拷贝过来;而展开命名空间是把命名空间的内容暴露在全局域中。

                ② 在项目中不推荐使用,因为很可能会产生命名冲突。

3、使用 using 展开命名空间中的某变量/函数/结构

        此方法为上面两种方法的折中使用:

        命名空间中某个变量使用频繁且展开后不会命名冲突,但某个变量不经常使用但有命名冲突,这种情况下可以展开命名空间中常用的变量即可,相当于把某个变量暴露在全局中

        使用方法如下:

         此方法不需要写命名空间namespace的关键字。

        如下代码所示:

namespace zyb
{
	namespace a
	{
		int i = 10;
	}

	namespace a
	{
		struct ii
		{
			int num1;
			double num2;
		};
	}
}
using zyb::a::i;
int main()
{
	i = 20;//可直接使用
	struct zyb::a::ii s1 = {s1.num1 = 10, s1.num2 = 1.2f};//还需要域作用限定符进行使用
}

五、C++的输入与输出

        C++的输入与输出需要包含头文件<iostream>,是 input output stream 的缩写,是C++标准的输入、输出流库,其中定义了标准的输入、输出对象(C++中的对象可以理解成C中的变量)。

(一)标准输入与输出

        ① std::cout 标准输出

        cout 是ostream的对象,主要面向窄字符(narrow characters(of type char))的标准输出流,作用是把字符以外的类型转化成字符流(字符串),再进行输出

        ② std::cin 标准输入

        cin 是istream的对象,主要面向窄字符(narrow characters(of type char))的标准输入流,作用是得到字符流后进行解析,转化成对应的整形或浮点数等类型,再输入到内存中

        注意:因为 cin 与 cout 都在C++标准库中,而标准库又在命名空间std中,要使用【域作用限定符】才能对 cin 与 cout 进行使用

问题:为什么标准输入、输出流要带个c?

回答:因为只有在内存中才存在整形,浮点数等类型;而在其他设备,比如文件、网络、磁盘等中,只支持字符。比如要将整形结果输出到控制台上,就要把整形转化成字符;其他设备上的字符类型数据也要转化后再进入内存进行处理。

(二)流运算符

        << 是流插入运算符,配合cout使用;>> 是流提取运算符,配合cin使用。

        std::cout << 的对象可以是任意类型的对象(变量),作用为把右边的对象进行输出到控制台;std::cin >> 的作用是提取流中的字符串,进行转化成对应的类型后进行输入到内存。

        C++把C中的左、右移运算符 << 、>> 进行了复用。

        使用例如下:

#include<iostream>

int main()
{
	int i;
	std::cin >> i;
	std::cout << i;

	return 0;
}

       结果如下:

        注意:

                ① << 、>> 支持连续的流插入与流提取,且每次连续的流插入与流提取都可以是不同的类型,因为运算符能够自动识别对象的类型;

                ② << 流插入的运算过程是从到右依次进行输出,>> 流提取的运算过程是从到右依次进行输入。

                如下所示:

int main()
{
	int i;
	double a;
	std::cin >> i >> a;
	std::cout << i << a;

	return 0;
}

        意思是:std::cin 提取字符内容转化后先给变量 i ,然后给变量 a;std::cout 先输出变量 i,然后输出变量 a 到控制台。

(三)换行

        有两种换行方法:

方法一:std::cout << i << '\n';

方法二:std::cout << i << std::endl;

        std::endl 是一个函数,流插入输出时,相当于插入一个换行字符+刷新缓冲区。

        std::endl 后面endl的全程为:end of line。

        std::endl的作用可以理解为是C中的换行符 '\n' ,但实际std::endl函数很复杂。

        cout / cin / endl 等都属于C++标准库,C++标准库都放在一个叫 std(standard) 的命名空间中,所以要通过命名空间的使用方式去用他们。

问题:既然可以使用\n进行换行,那为什么还需要使用std::endl呢?

回答:不同类型平台的换行符不一样,可能换行符 '\n' 在另一平台并不适用,而std::endl会适配当前平台的换行符,保证了代码的可移植性。

        

(四)std::cout 和 std::cin 与 printf 和 scanf 的对比

        ① printf 和 scanf 需要按格式指定类型,而 << 流插入可以自动识别对象的类型,无需再进行格式化的指定%d,%s等;

        他们效果是一样的,都是进行输入与输出,且这四个函数可以混合着用,没有影响;

        若想控制输出的小数点或者输出宽度,建议使用printf,这样更加方便。

        ② cin 与 cout 的效率比 printf 和scanf 要低,因为cin 与 cout 底层是存在缓冲区的,遇到刷新标准才会进行输入输出;且流之间有绑定关系,一个流没遇到刷新标准而后面的流遇到了,系统为了同步两个流,就会调用刷新指标把前一个流推出去,这些调用对系统都有开销,所以效率会低。

        ③ <iostream>中包含了<stdio.h>,只包含<iostream>也能使用 printf 和 scanf。vs系列的编译器是这样,其他编译器可能会报错。

        现在应该能了解C++的第一个程序了。

六、缺省参数

        缺省参数是声明或者定义函数时为函数的参数指定一个缺省值。

        作用:在调用该函数时,如果没有指定实参,则采用该形参的缺省值;若指定了实参,则会替换掉形参的缺省值,这样可以使函数的调用更加灵活。

        缺省参数可以理解为默认参数

        使用示例如下:

void Add(int a = 0, int b = 1)//这里的 0 和 1 就是该函数的缺省值
{
    //业务代码
}

int main()
{
    Add();
    return 0;
}

        缺省参数又分为全缺省和班缺省参数:

                ① 全缺省参数:全部形参都给缺省值;

                ② 半缺省参数:部分形参给缺省值,C++规定了半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。如下所示:

void Add(int a , int b = 1, int c) 错误赋缺省值
{
    //业务代码
}

        注意:

                ① 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。如下所示:

void Add(int a = 5, int b = 1, int c = 7) 错误赋缺省值
{
    //业务代码
}

int main()
{
    Add( , 8, ); 错误传值给缺省函数
    return 0;
}

                ② 函数声明与函数定义分离时,缺省参数不能再函数声明和定义中同时出现,规定必须函数声明给缺省参数。

                ③ C语言中无缺省参数。

        缺省的实际应用:

                若不知道要传什么参数给函数,就用缺省值;若明确要传什么值就直接传参,替换掉缺省值。

        总结:

                缺省值又叫默认值,若函数不传参就使用缺省值,若传参就使用传递过来的参数替换掉缺省值。

七、函数重载

        

        C++支持在同一作用域中出现同名函数,不过要求这些同名函数的形参不同:参数个数不同,参数类型不同,参数顺序不同(本质还是参数类型不同)。这样C++函数调用就出现了多态行为,使用更加灵活。C语言不支持函数重载。

        示例如下:

#include<iostream>
using namespace std;

// 1、参数类型不同
void Add(int a, int b)
{
	cout << "int: a + b" << endl;
}
void Add(double a, double b)
{
	cout << "double: a + b" << endl;
}


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

int main()
{
	Add(1, 2);
	Add(1.2f, 2.5f);//参数类型不同

	f();
	f(1);//参数个数不同

	f(1, 'a');
	f('a', 1);//参数顺序不同

	return 0;
}

        结果如下:

         注意:

                ① 返回值不同时不是函数重构,因为调用时也无法区分。若返回值相同但类型不同,是属于重构的,如下所示,代码会报错:

                ② 当同名函数一个没有参数,一个是缺省参数的时候,语法上是构成重载的,但实际用起来还是会报错,因为使用时有歧义,编译器不知道调用谁。如下所示:

八、引用 

(一)引用的概念和定义 

1、概念

        引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如孙悟空的别名叫齐天大圣,猪八戒叫他大师兄,指的都是同一个人物。

2、定义

        引用的定义如下:

         这里的&其实就是对C语言中的取地址符进行了复用,成为了引用符。

        示例代码如下:

#include<iostream>
using namespace std;

int main()
{
	int num = 10;
	int& i = num;
	i = 250;
	cout << num << endl;

	return 0;
}

        结果为:

        内存的示例如下:

int main()
{
	int a = 0;
	int& b = a;
    int& c = b;
    int& d = c;

	return 0;
}

        以上代码的内存:

        指向的都是同一块空间,本质是同一个空间有多个名称

3、引用 与 typedef 和 define 的区别

        ① 引用是给变量取别名,如 :

int& 齐天大圣 = 孙悟空;

        ② typedef是给类型取别名;

typedef unsigned int uint;

        ③ define是定义一个宏,预处理时进行替换。

#define a 10;

(二)引用的特性

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

        如下所示:

        所以不存在空引用,但是有空指针

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

        如下所示:

#include<iostream>
using namespace std;

int main()
{
	int num = 10;
	int& a = num;
	int& b = num;
	a = 1000;
	cout << b << endl;

	return 0;
}

        结果为:

3、引用过一个实体后,不能改变指向

        如下代码所示:

#include<iostream>
using namespace std;

int main()
{
	int num1 = 10;
	int& a = num1;

	int num2 = 786;
	a = num2;
	cout << "num1的地址:" << &num1 << endl \
		<<  "a的地址:   " << &a << endl \
		<<  "num2的地址:" << &num2 << endl;

	return 0;
}

        在X86环境下的结果为:

        此处 a = num2 并不是改变指向,而是赋值。

        正是因为不能改变指向,所以引用不能替代指针;指针与引用是相辅相成的关系,而不是替代关系。比如在链表中有三个链接的节点,若删除了中间的节点,第一个节点的指针域就要改变指向,指向第三个节点的数值域;而引用就做不到这一点。若是在函数参数中接收指针地址,就不用创建二级指针了,直接引用指针的别名就可以做到对同一块的使用了,这样会更加简洁易懂。

(三)引用的使用

        引用在实际应用中主要干的两件事:【在引用传参和引用做返回值中减少拷贝提高效率】和【改变引用对象时同时改变被引用的对象】。简单来说,就是提高传参和返回值的效率,和能够像指针一样改变同一块地址。

        ① 引用参数如下:

引用传参
struct arr
{
	int a[1000];
};

void Fun1(struct arr& ii)
{
	//业务代码;
}

int main()
{
	struct arr i = { {0} };
	Fun1(i);
	return 0;
}
不用再拷贝数组进函数里,直接对原数组进行处理,
极大提高效率

        ② 引用返回值(前提是返回值出了函数后不能被销毁):

                一般的函数返回值是先把返回值临时拷贝到一个临时空间中,两者属于不同的空间,再把临时空间的值作为函数调用的返回值,若直接在调用函数处进行运算就会出错,因为运算是对临时空间中的值进行计算,而不是对真正的返回对象做运算。

                若把函数的返回类型设置成【类型&】,这样返回的就是返回值的别名,这样返回值与函数调用处指向的是同一块空间,可以直接对函数的返回值处做运算。这样减少了系统对临时空间开辟的开销,增加了返回的效率。(也可以把返回类型写成指针类型,然后返回值写成地址,这样函数调用处就要解引用,也可以达到目的,但比引用的书写要更麻烦一些)

                示例如下所示:

#include<iostream>
using namespace std;
struct Stu
{
	int age;
}stu;
int& Fun2(int i)
{
	i++;
	return i;
}
int main()
{
	stu.age = 18;
	cout << ++Fun2(stu.age) << endl;
	return 0;
}

        结果为:

                注意:并不是全部情况都能够使用引用返回。比如临时变量的返回使用引用是不合法的,会越界访问。(使用指针返回也是野指针)

  

        函数的形参也可以是引用参数,这样形参与实参指向的是同一块空间,这样的传值类似于传址调用(指针传参)

        如下:

改变引用对象时同时改变被引用的对象

#include<iostream>
using namespace std;

void Add(int& a)
{
	a = a + 3080;
}

int main()
{
	int i = 10;
	Add(i);
	cout << i << endl;
	return 0;
}
直接对同一块空间做处理

        结果为:

        注意:

                ① 能对指针变量进行引用,这样对函数传指针就不许要二级指针了,相对而言简化了程序。

                如下所示:

int main()
{
	int i = 10;
	int* p = &i;
	int*& ip = p;
	return 0;
}

                ② 与指针的区别:引用不能改变指向,不能完全替代指针。

(四)const引用

1、const引用与const对象

        可以引用一个const对象,但是必须用const引用,否则会报错:

        因为变量被const修饰后的访问权限会变小,若直接引用被const修饰的变量,就属于访问权限放大,这是禁止的;可以使用const引用来引用普通对象,这样是属于把普通对象的访问权限缩小,是可以的。(对象的访问权限在引用过程中可以缩小,但不能放大,相当于只能向下兼容

        形象理解:对象就是老大,引用就是小弟,老大下了规定而小弟就不能做规定外的事情;而老大没有规定,那么小弟给自己规定是没问题的。

        如下所示:

        这种情况下 i 能修改, 而 num 不能修改,因为 num 的权限不会影响 i 。

        注意:只有引用和指针才存在访问权限的放大和缩小问题。

        此处 *p 是不能被修改的,若赋值给指针 a ,那么 *a 就能修改 i,这属于访问权限的放大。

2、临时对象触发权限放大

        需要注意的是类似 【int& rb = a*3; 】【double d = 12.34; 】【int& rd = d;】 这样⼀些场景下a*3表达式的结果保存在一个临时对象中,【int& rd = d】也是类似,类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性(常量的性质,与被const修饰一样),所以这里就触发了权限放大,必须要用const引用才可以

        所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象

        总结:【表达式】与【类型转化】的接收会创建临时对象,而临时对象具有常量的性质,会缩小访问权限,要使用const引用。

3、const引用的使用

        通常使用于函数的参数接收:

void Fun(const int& i);

int main()
{
    Fun(10);
    return 0;
}

        这样就会保证参数的接收的范围变大(包括:① const对象, ② 普通对象, ③ 临时对象),且不会扩大访问权限

(五)指针和引用的关系

        C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。

        • 语法概念上引用是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间

        • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

        • 引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象。

        • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。

        • sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。(32位平台下占4个字节,64位下是8个字节)

        • 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。

        注意:

                删除空指针是无害的,因为指针变量与指向的变量是两个空间;

                而删除引用会导致原空间也会被删除。

九、inline 内联函数

        用 inline 修饰的函数叫做内联函数编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以减少开销提高效率。

        如下所示:

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

int main()
{
	int num1 = 5;
	int num2 = 6;
	Add(num1, num2);

	return 0;
}

        inline 对于编译器而言只是一个建议,也就是说,你加了 inline 编译器也可以选择在调用的地方不展开,不同编译器关于 inline 什么情况展开各不相同,因为C++标准没有规定这个。inline 适用于频繁调用的短小函数(八九行左右),对于递归函数,代码相对多一些的函数,加上 inline 也会被编译器忽略。

        宏中有很多的坑:多用括号解决优先级问题结尾不要写分号多增加一个空语句,但是宏的替换机制可以使调用时不用建立栈帧,提高效率。

        而 inline 就是解决宏的问题而设计出来的,既没有宏的坑,也会提高效率。唯一的缺点就是 inline 对于编译器来说是个建议,无法做到百分比替换。

        vs 编译器 debug 版本下⾯默认是不展开 inline 的,这样方便调试,debug 版本想展开需要设置⼀下以下两个地方

         展开后的底层汇编代码如下,没有 call 指令,已展开代码:

	int num1 = 5;
00007FF73A1C1562  mov         dword ptr [num1],5  
	int num2 = 6;
00007FF73A1C156A  mov         dword ptr [num2],6  
	Add(num1, num2);

	return 0;
00007FF73A1C1572  xor         eax,eax 

        判断是否展开:

        函数被编译后是一堆指令,需要储存起来执行。第一句指令的地址就是函数的地址。(数字后面的 h 是16进制的后缀)

        汇编指令中,只要有 call 指令,就是建立函数栈帧了,就不展开。

        注意:

                ① 为什么代码过长就不展开了:因为代码过长展开后会使可执行文件(.exe)膨胀,加载进计算内存的进程中也会使进程膨胀,导致执行缓慢等问题。所以展开的决策权交给程序来判断。

                ② inline 不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,在底层汇编无法使用 call 指令,链接时会出现报错。

                解决:声明和定义写一起。

                ③ 可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数,因为 inline 函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突。

十、nullptr

        

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

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

        在c++中,NULL被定义成 0 ,而在c中,被定义成地址为0的泛型指针。

        在如下代码中就会出错:

#include<iostream>
using namespace std;

void f1(int i)
{
	cout << "f1(int i)" << endl;
}

void f1(int* ptr)
{
	cout << "f1(int* ptr)" << endl;
}

int main()
{
	f1(0);
	f1(NULL);
	return 0;
}

        结果如下:

        都调用第一个代码了,C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过 f1(NULL) 调用指针版本的 f1( int* ptr) 函数,但是由于NULL被定义成0,因此调用了 f1(int i) 函数,因而与程序的初衷相悖,若使用 f1((void*)NULL),则调用会报错。

        如下所示:

        为了解决这个问题。C++11中引入 nullptr,nullptr 是一个特殊的关键字,是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型(自动识别)。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被隐式地转换为指针类型,因而不能被转换为整数类型。

#include<iostream>
using namespace std;

void f1(int i)
{
	cout << "f1(int i)" << endl;
}

void f1(int* ptr)
{
	cout << "f1(int* ptr)" << endl;
}

int main()
{
	f1(0);
	f1(nullptr);
	return 0;
}

        结果为:

        在C++中空指针就用 nullptr 进行接收 

十一、有意思的点

(一)越界访问不一定报错

        越界读,不报错。

int main()
{
	int arr[10] = { 0 };
	cout << arr[11] << endl;
	cout << arr[12] << endl;
	cout << arr[13] << endl;
	return 0;
}

        结果为:

       

        越界写不一定报错,一般是抽查

        如下演示:

int main()
{
	int arr[10] = { 0 };
	arr[10] = 1;//报错
	arr[11] = 1;//报错
	arr[12] = 1;//不报错

	return 0;
}

        因为vs数组的后两位为抽查位,过了抽查位再进行修改就不会报错;不同平台,不同编译器的结果不同。

(二)引用与指针的底层

        演示代码如下:

int main()
{
	int i = 10;
	int& num = i;
	num++;
	int* p = &i;
	*p++;

	return 0;
}

        部分反汇编代码如下:

	int i = 10;
00007FF6F4091DEE  mov         dword ptr [i],0Ah  
	int& num = i;
00007FF6F4091DF5  lea         rax,[i]  
00007FF6F4091DF9  mov         qword ptr [num],rax  
	num++;
00007FF6F4091DFD  mov         rax,qword ptr [num]  
00007FF6F4091E01  mov         eax,dword ptr [rax]  
00007FF6F4091E03  inc         eax  
00007FF6F4091E05  mov         rcx,qword ptr [num]  
00007FF6F4091E09  mov         dword ptr [rcx],eax  
	int* p = &i;
00007FF6F4091E0B  lea         rax,[i]  
00007FF6F4091E0F  mov         qword ptr [p],rax  
	*p++;
00007FF6F4091E13  mov         rax,qword ptr [p]  
00007FF6F4091E17  add         rax,4  
00007FF6F4091E1B  mov         qword ptr [p],rax  

	return 0;
00007FF6F4091E1F  xor         eax,eax  

        mov汇编指令为移动;lea汇编指令为取地址,[ ] 为取地址。

        可以看到,在语法层面上引用不开空间,而指针开辟空间;但是在底层汇编层面,是没有引用概念的,引用也是用指针实现的,几乎相同

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2103388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何从 SIM 卡恢复短信?这篇专业教程文章会帮到你

您是 Android 手机的忠实用户吗&#xff1f;您是否定期通过打电话、发送短信或在 WeChat 上聊天来与朋友和家人保持联系&#xff1f;如果你这样做&#xff0c;我相信你的手机上一定有很多短信&#xff0c;这些短信对于保存你与他人聊天的内容非常重要。您是否有这样的经验&…

碧桂园服务上海项目获评“2024年度上海市物业管理优秀示范项目”

近日&#xff0c;上海市物业管理行业协会发布《关于2024年度上海市物业管理优秀示范项目评定结果的公示》官方红头文件。文件对上海市393个物业管理优秀示范项目进行公示。 上海联源物业发展有限公司露香园项目、静鼎安邦府邸项目、佘山东郡项目和上海金晨物业经营管理有限公司…

mac 安装brew并配置国内源

​ 前置条件 - Xcode 命令行工具 一行代码安装Homebrew 添加到路径(PATH) - zsh shell为例 背景介绍 最近重装了我的MAC mini &#xff08;m1 芯片&#xff09;, 很多软件都需要重新安装&#xff0c;因为后续还需要安装一些软件&#xff0c;所以想着安装个包管理软件 什么…

苹果手机通话记录怎么恢复?已总结了4个方法,快速恢复

苹果手机的通话记录是我们联系好友与家人的重要方式之一。如果我们忘记储存重要好友或家人的联系方式&#xff0c;但是曾经有过通话&#xff0c;那我们就可以在苹果手机的通话记录中找到重要的联系人。但是&#xff0c;如果这些通话记录不小心被删除了&#xff0c;那苹果手机通…

ArkUI-状态管理最佳实践

ArkUI-状态管理最佳实践 概述合理选择装饰器使用监听和订阅精准控制组件刷新Watch装饰器监听数据源使用自定义事件发布订阅 概述 在声明式UI编程范式中&#xff0c;UI是应用程序状态的函数&#xff0c;应用程序状态的修改会更新响应的UI界面。ArkUI采用了MVVM模式。 ArkUI提…

2024年第十届数维杯国际大学生数学建模挑战赛

竞赛介绍 为了培养学生的创新意识及运用数学方法和计算机技术解决实际问题的能力&#xff0c;内蒙古创新教育学会、内蒙古基础教育研究院决定主办2024年第十届数维杯国际大学生数学建模挑战赛&#xff08;国际赛&#xff09;。 数维杯大学生数学建模挑战赛每年分为两场&#…

uniapp开发鸿蒙,是前端新出路

uniapp开发鸿蒙&#xff0c;是前端新出路吗&#xff1f; 相信不少前端从业者一听uniapp支持开发鸿蒙Next后非常振奋。小编作为7年前端也是非常激动&#xff0c;第一时间体验了下。在这里也给大家分享一下我的看法 uniapp开发鸿蒙优势 1.对于前端开发者而言&#xff0c;几乎无需…

基于51单片机的跑马串口调试波形发生器proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1WTjU_hRJ-fLMTT5g1q-NlA 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectro…

嵌入式全栈开发学习笔记---C++(多态)

目录 多态polymorphic 多态成立的三个条件 1、要有继承 2、要有虚函数重写 3、用父类指针(父类引用)指向子类对象 重载与重写区别 动态联编和静态联编 多态原理 构造函数中调用虚函数能否实现多态&#xff1f; 用父类指针指向子类数组 虚析构函数 动态类型识别 第…

【免越狱】iOS任意版本号APP下载

下载地址 https://pan.quark.cn/s/570e928ee2c4 软件介绍 下载iOS旧版应用&#xff0c;简化繁琐的抓包流程。一键生成去更新IPA&#xff08;手机安装后&#xff0c;去除App Store的更新检测&#xff09;。 软件界面 使用方法 一、直接搜索方式 搜索APP&#xff0c;双击选…

Vue3 + Ts + Vite项目 websoket封装使用

文章目录 一、安装二、封装三、请求地址配置3.1 将接口地址放到 public3.2 引入 ipconfig.js 文件3.3 全局类型声明 四、页面使用4.1 引用4.2 注册 五、说明 一、安装 npm npm install websocket --save-devpnpm pnpm install websocket --save-dev二、封装 在 /src/utils …

Haskell爬虫:连接管理与HTTP请求性能

爬虫技术作为数据抓取的重要手段&#xff0c;其效率和性能直接影响到数据获取的质量与速度。Haskell&#xff0c;作为一种纯函数式编程语言&#xff0c;以其强大的类型系统和并发处理能力&#xff0c;在构建高效爬虫方面展现出独特的优势。本文将探讨在Haskell中如何通过连接管…

OVMR:华为北大联手,基于多模态融合的SOTA开放词汇识别 | CVPR 2024

即插即用的方法OVMR将新类别的多模态线索嵌入到VLM中&#xff0c;以增强其在开放词汇识别中的能力。它最初利用多模态分类器生成模块将示例图像嵌入到视觉标记中&#xff0c;然后通过推断它们与语言编码器的上下文关系来自适应地融合多模态线索。为了减轻低质量模态的负面影响&…

[DICOM活久见] 序列内部的RescaleIntercept不同导致的问题

本文由Markdown语法编辑器编辑完成. 1. 背景: 本文记录在工作中遇到的一些比较罕见的dicom图像. 这对于在未来工作中, 处理图像时, 需要考虑方案的完整性, 会有很大的帮助. 本文介绍的, 是目前我工作10年来, 头一次见到的一个CT序列, 它的序列内的RescaleIntercept值, 不是完…

Ubuntu解压7z压缩包方法

0 Preface/Foreword 1 解压缩指令 1.1 环境安装和检查 环境&#xff1a;检测ubuntu环境是否装有7z工具&#xff0c;如果没有&#xff0c;需要手动安装&#xff0c;安装方法如下&#xff1a; sudo apt-update sudo apt-get install p7zip-full 检测工具是否安装成功&#xff…

qmt量化交易策略小白学习笔记第56期【qmt编程之期权数据--获取历史期权列表--原生Python】

qmt编程之获取期权数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取历史期权列表 函数能帮助用户获取历史期权列表, 包括某日历史在上交所上市的认购合约和认沽合约, 也包括已经退市的…

PTA单词首字母大写

作者 颜晖 单位 浙大城市学院 本题目要求编写程序&#xff0c;输入一行字符&#xff0c;将每个单词的首字母改为大写后输出。所谓“单词”是指连续不含空格的字符串&#xff0c;各单词之间用空格分隔&#xff0c;空格数可以是多个。 输入格式: 输入给出一行字符。 输出格式…

Css:属性选择器、关系选择器及伪元素

css的属性选择器&#xff1a; 注&#xff1a;属性值只能由数字&#xff0c;字母&#xff0c;下划线&#xff0c;中划线组成&#xff0c;并且不能以数字开头。 1、[属性] 选择含有指定属性的元素&#xff0c;用[]中括号表示。 <style> /*注意大小写区分 注意前后顺序 样…

电脑技巧:如何在Win11电脑上调整设置,让屏幕更加护眼?

目录 一、调整屏幕亮度 二、启用夜间模式 三、调整色彩设置 四、使用第三方护眼软件 五、保持良好的用眼习惯 总结 随着长时间使用电脑的人越来越多,护眼问题也变得越来越重要。Win11作为更新的操作系统,提供了更多的设置选项来帮助我们保护眼睛。本文将详细介绍如何在…

针对实验试服务器的使用问题【安装anaconda+Vscode连接+jupyther远程连接】

目录 一、Xshell连接 1-创建连接 2-安装anaconda 3-创建conda环境 4-修改下载镜像源 5-安装torch 二、VScoda连接服务器 1-下载插件Remote-ssh 用于远程连接服务器 2-配置文件 三、jupyther远程连接 四、师兄推荐的入门资料 1-python基础 2-机器学习 五、参考资料…