【C++】初识C++(2)

个人主页@我要成为c嘎嘎大王

希望这篇小小文章可以让你有所收获!

目录

[一 、引用](#一 、引用)

[1.1 引用的概念和定义](#1.1 引用的概念和定义)

[1.2 引用的特性](#1.2 引用的特性)

[1.3 引用的使用](#1.3 引用的使用)

[1.3.1 引用作参数](#1.3.1 引用作参数)

[1.3.2 引用作返回值](#1.3.2 引用作返回值)

[1.3.3 引用与指针的区别](#1.3.3 引用与指针的区别)

[1.3.4 const引用](#1.3.4 const引用)

[二、inline 内联函数](#二、inline 内联函数)

[2.1 内联函数概念](#2.1 内联函数概念)

[2.2 内联函数特性](#2.2 内联函数特性)

三、auto关键字

[3.1 auto常用于类型推导](#3.1 auto常用于类型推导)

[3.2 auto从C++11标准及更高标准下可以作返回类型](#3.2 auto从C++11标准及更高标准下可以作返回类型)

[3.3 auto不能作为函数的参数](#3.3 auto不能作为函数的参数)

[3.4 auto不能用来直接声明数组](#3.4 auto不能用来直接声明数组)

四、基于范围的for循环(C++11)

五、nullptr


一 、引用

1.1 引用的概念和定义

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

格式: 类型& 引用别名=引用对象;

1.2 引用的特性

  • 引用在定义时必须初始化
  • ⼀个变量可以有多个引用
  • 引用⼀旦引用⼀个实体,再不能引用其他实体

1.3 引用的使用

1.3.1 引用作参数

例如: 交换两个数

在C语言中,用代码实现如下。交换两个数要把地址传过去,形参是实参的临时拷贝,只有把地址传过去,参数才可以改变。

cpp 复制代码
void swap(int* x ,int* y) 
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main() 
{
	int a = 10;
	int b = 20;
	swap(&a,&b);	//把地址传过去
	return 0;
}

在C++中,使用引用,引用和原值是共用一块内存空间。

这里的x和y分别是a和b的别名。

cpp 复制代码
void swap(int& x,int &y) 	//引用做参数
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main() 
{
	int a = 10;
	int b = 20;
	swap(a,b);
	return 0;
}

引用做参数,可以减少拷贝,提高效率。

1.3.2 引用作返回值

cpp 复制代码
int& func()		//返回值类型 int& ,传引用返回
{
	int a = 1000;
	return a;
}
int main()
{
	int ret = func();
	cout << ret << endl;
	return 0;
}

int func 传值返回时(返回的是拷贝),函数栈帧的销毁,会导致a的值可能是随机值。

int& func 传引用返回,返回a的别名,但是指向的内存空间没有改变,函数栈帧的销毁,内存返回给操作系统,引用的值也变成了随机值。(出现了野引用)

什么情况下可以使用引用返回?

全局变量/静态变量/堆上的空间等可以作引用返回。

1.3.3 引用与指针的区别

cpp 复制代码
int main()
{
	//引用 
	int a = 10;
	int& ra = a;  //语法层面不开空间
	ra = 20;
	//指针
	int* pa = &a; //语法层面开空间
	*pa = 20;
	return 0;
}

语法上(规定用法) :

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

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

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

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

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

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

• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
底层上(汇编) :

引用的底层使用指针实现的。

汇编层面上,没有引用,都是指针,引用编译后也转换成指针了。

引用和指针的功能类似,但是引用不能完全替代指针,因为引用定义后不能改变指向。例如:在单链表中,删除链表中的某一个结点,需要让指针指向要删除结点的下一个结点(改变指针的指向)。但是引用是无法进行改变指向的。

1.3.4 const引用

可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	const int a = 10;
	//编译报错:error C2440 : "初始化" :无法从"const int"转换为"int& "
	//这里的引用是对a访问权限的放⼤
	//int& ra = a;
	
	//这样才可以
	const int& ra = a;
	// 编译报错:error C3892 : "ra":不能给常量赋值
	//ra++;
	
	//这里的引用是对b访问权限的缩⼩
	int b = 20;
	const int& rb = b;
	//编译报错:error C3892 : "rb":不能给常量赋值
	//rb++;
	return 0;
}

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

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

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& ra = 30;
	//编译报错"初始化" :⽆法从"int"转换为"int& "
	//int& rb = a * 3;
	//正确写法:
	const int& rb = a * 3;

	double d = 12.34;
	//编译报错:"初始化" :⽆法从"double"转换为"int& "
	//int& rd = d;
	//正确写法:
	const int& rd = d;

	return 0;
}

二、inline 内联函数

2.1 内联函数概念

内联函数:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。

在C语言中,如果进行多次调用函数,那么会多次建立函数栈帧,C语言是通过宏函数来解决这个问题。因为宏会在预处理阶段进行宏的替换,这样就不会建立函数栈帧了。

宏的优点:

没有函数栈帧的创建,提高效率。

但是宏也是有一些缺点的:语法复杂,坑很多,不容易控制;不能调试;没有类型安全的检查。

在C++中,在调用内联函数的地方展开,没有函数调用建立栈帧的开销

为什么内联函数适合短小函数(通常1~5行)呢?

假设 func函数有100行(大型函数),项目中有1w个调用该函数的位置。

内联展开了 100*1w = 100w行。

不使用内联展开 100+1w = 1.01w行。

大的函数就影响很大,不适合使用内联函数,所以短小函数可以换用内联函数。

在内联函数在长一点的函数和递归函数是不会展开的。

2.2 内联函数特性

内联函数(inline)是一种以空间换时间的做法,在编译阶段,会把函数展开(在调用点处)。

优点:减少了函数栈帧的开销,提高效率;

缺点:目标文件可能会增大。

内联函数适合较小代码量的函数

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

三、auto关键字

3.1 auto常用于类型推导

在C++中,auto有许多作用,但是最常用的是用于类型推导。

cpp 复制代码
#include<iostream>
using namespace std;

int main(){
	int i = 0;
	auto k = i;	//auto 在这里的作用就是自动(识别)推导类型

	void(*pf1)(int, int) = func;	//函数指针
	auto pf2 = func;				//自动识别函数指针
	//打印类型进行验证一下、
	cout << typeid(pf1).name() << endl;//void (__cdecl*)(int,int)
	cout << typeid(pf2).name() << endl;//void (__cdecl*)(int,int)
	
	std::map<std::string, std::s>::iterator it = dict.begin();
	auto it = dict.begin();	
}

注意:

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

3.2 auto从C++11标准及更高标准下可以作返回类型

cpp 复制代码
auto func(int a,int b) {}	

3.3auto不能作为函数的参数

cpp 复制代码
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a){}

3.4 auto不能用来直接声明数组

cpp 复制代码
int a[] = {1,2,3};
auto b[] = {4,5,6};	//error

四、基于范围的for循环(C++11)

范围for的语法

C++11中引入了基于范围的for循环。for循环后的括号由冒号" :"分为两部分:第一部分 是范围内用于迭代的变量,第二部分则表示被迭代的范围。

cpp 复制代码
#include<iostream>
using namespace std;
int main(){
    int a[6] = {1,2,3,4,5,6};
    int b[6] = {1,2,3,4,5,6};
	//普通的for循环
	for (int i = 0; i < sizeof(a) / sizeof(a[0]);i++)
	{
		a[i] *= 2;
	}
	for (int i = 0; i < sizeof(a) / sizeof(a[0]);i++)
	{
		cout << *(a + i) <<' ';	//打印结果:2 4 6 8 10 12
	}
    
    //范围for的使用 (C++11)
	for (int e:b){	//依次取数组中赋值给e,自动迭代,自动判断结束
		e *= 2;
    }
	for (int e : b){
		cout << e << ' ';	//打印结果:1 2 3 4 5 6
    }
}

上述范围for的使用C++11也进行了数值乘2,但是打印结果没有发生变化。原因是: 因为是从数组中取出赋值给e,并没有改变数组中的数据,所以打印还是原值。

要想修改原数组的值,则需要使用引用

cpp 复制代码
#include<iostream>
using namespace std;
int main(){
    int b[6] = {1,2,3,4,5,6};
    //范围for的使用 (C++11)
	for (int& e : b){	//依次取数组中赋值给e,自动迭代,自动判断结束
		e *= 2;
    }
	for (int e : b){
		cout << e << ' ';	//打印结果:2 4 6 8 10 12
    }
}

范围for循环可以用continue来结束本次循环,也可以用break来跳出整个循环。

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

五、nullptr

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

cpp 复制代码
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0  
    #else
        #define NULL ((void *)0)   
    #endif
#endif
  • C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过f(NULL)调用指针版本的 f(int*)函数,但是由于NULL被定义成0,调用了f(intx),因此与程序的初衷相悖。f((void*)NULL); 调用会报错。
  • C++11中引入nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
cpp 复制代码
#include<iostream>
using namespace std;
void f(int x){
	cout << "f(int x)" << endl;
}
void f(int* ptr){
	cout << "f(int* ptr)" << endl;
}

int main(){
	f(0);
	// 本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(intx),因此与程序的初衷相悖。
	f(NULL);
	f((int*)NULL);

	// 编译报错:error C2665 : "f": 2个重载中没有一个可以转换所有参数类型
	// f((void*)NULL);
	f(nullptr);
	return 0;
}

nullptr的主要优势在于它提供了更好的类型安全性。它只能被赋值给指针类型,而不能被赋值给整数类型,这有助于减少因错误地将NULL或0用于非指针类型而引起的问题。此外,使用 nullptr 可以避免与某些平台上的空指针值冲突。

在C++11标准中,sizeof(nullptr) 和sizeof((void*)0)所占字节数相同,后续表示指针空值时,建议使用nullptr。

希望这篇小小文章可以为你解答疑惑!

若上述文章有什么错误,欢迎各位大佬及时指出,我们共同进步!

相关推荐
神仙别闹25 分钟前
基于C#+SQlite开发(WinForm)个人日程管理系统
开发语言·jvm·c#
超级码.里奥.农40 分钟前
零基础 “入坑” Java--- 十二、抽象类和接口
java·开发语言
肥or胖1 小时前
【音视频协议篇】RTSP系列
c++·笔记·音视频
WebGoC开发者1 小时前
C++题解(37) 信息学奥赛一本通1318:【例5.3】自然数的拆分
c++·算法·青少年编程·题解
iFlyCai1 小时前
Flutter状态管理篇之ChangeNotifier基础篇(一)
开发语言·flutter·dart
Aurora_wmroy1 小时前
算法竞赛备赛——【图论】求最短路径——小结
数据结构·c++·算法·蓝桥杯·图论
whoarethenext2 小时前
使用 C++ 和 OpenCV 进行表面划痕检测
开发语言·c++·opencv·划痕检测
tomato092 小时前
Codeforces Round 1037 (Div. 3)(补题)
c++
遇见尚硅谷2 小时前
C语言:20250719笔记
c语言·开发语言·数据结构·c++·算法
mit6.8242 小时前
7.17 滑动窗口 |assign |memo |pii bfs |位运算
c++·算法