目录
一、引用
1.1 引用的概念
在C++中,引用是一种别名,用于给已存在的变量起一个新的名称。引用不是新定义一个变量,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
定义命名空间的格式如下:
类型& 引用变量名(对象名) = 引用实体;
(注:引用类型必须和引用实体是同种类型的)
void test1()
{
int a = 10;
int& ra = a;
//引用的类型: int
//引用变量名(对象名): ra
//引用实体:a
a = 20;
cout << "a = " << a << endl;
cout << "ra = " << ra << endl;
ra = 30;
cout << "a = " << a << endl;
cout << "ra = " << ra << endl;
printf("%p\n%p\n", &a, &ra);
}
通过地址可以看到:引用和引用的变量共用同一块内存空间
1.2 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
void test2() { //1. 引用在定义时必须初始化 int a = 10; //int& ra;//报错 int& ra = a; //2. 一个变量可以有多个引用 int& rra = a; cout << "a = " << a << endl; cout << "rra = " << rra << endl; printf("%p\n%p\n%p\n", &a, &ra, &rra); //3. 引用一旦引用一个实体,再不能引用其他实体 //int b = 5; //int& ra = b; }
1.3 常引用
C++中的常引用(const reference)是指在声明引用时加上 const 修饰符,使其成为一个只读的引用。
1.3.1 常引用的特点
常引用具有以下特点:
- 只读性:常引用不允许通过引用修改所引用对象的值,即不能对其进行赋值操作。它提供了一种方式来保证被引用对象的不可变性。
- 引用语义:常引用仅是对已存在对象的一个别名,通过常引用可以方便地访问和使用原始对象的值,而无需进行拷贝。
int test3()
{
const int b = 4;
//int& rb = b;//报错,b具有常量性质,为了防止被修改而报错
const int& rb = b;//不报错,rb为常引用,具备常量性质
int b1 = 4;
const int& rb1 = b;
//不报错,rb1是b1的别名,但是只能访问不能修改,相当于缩小了rb1的权限
//int& b2 = 4;//报错,b2指向的是常量,不能修改
const int& b2 = 4;//b2和常量4的权限相同,都是只能访问不能修改
double b3 = 0.44;
//int& rb3 = b3;//报错,指向的是double,如果进行运算可能会出错(算术运算或自增/减运算)
const int& rb3 = b3;//让引用不会改变原来的值就可以了
}
1.3.2 常引用的应用场景
- 确保对象的不可变性:对于不需要修改的对象,可以使用常引用来确保其不会被意外修改。
例如拷贝数据时,要保证被拷贝的数值不能发生改变。
注:常引用虽然不能修改所引用对象的值,但并不意味着所引用对象本身是常量。
常引用只是表明在引用的上下文中,引用的对象是只读的。
1.4 引用的使用
- 1. 避免不必要的拷贝:当函数返回一个临时对象或者其他大型对象时,可以将返回类型声明为常引用,避免不必要的对象拷贝。
下面是以后要学习的拷贝构造函数,现在先不用理解,只需要看到参数里面是一个常引用,
参数是一个栈,可能占用资源会比较多,如果不用常引用而是直接拷贝,会浪费较多的资源
(这里的拷贝构造函数只能使用常引用,原因我以后再说)Stack(const Stack& st) { int* tmp = (int*)malloc(sizeof(int) * st.capacity); if (nullptr == tmp) { perror("malloc error!"); exit(-1); } memcpy(tmp, st.arr, sizeof(int) * st.top); this->arr = tmp; this->capacity = st.capacity; this->top = st.top; }
- 2. 函数参数传递:通过常引用作为函数的参数,可以避免对实参进行拷贝,同时确保函数内部不会修改传入的对象
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
- 3. 做函数返回值:与做函数参数传递相同
int& test4() { static int c = 100; //... return c; }
注:
函数运行结束,出了函数作用域时,
- 如果返回对象还在(还没还给系统),则可以使用引用返回,
- 如果已经还给系统了,则必须使用传值返回
1.5 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
return 0;
}
int a = 10;
000F313F mov dword ptr [a],0Ah
int& ra = a;
000F3146 lea eax,[a]
000F3149 mov dword ptr [ra],eax
ra = 20;
000F314C mov eax,dword ptr [ra]
000F314F mov dword ptr [eax],14h
int* pa = &a;
000F3155 lea eax,[a]
000F3158 mov dword ptr [pa],eax
*pa = 20;
000F315B mov eax,dword ptr [pa]
000F315E mov dword ptr [eax],14h可以看到,引用和指针在底层的操作是相同的
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
二、内联函数(inline)
2.1 宏函数
在C语言中学习的宏函数是一种将代码片段替换为指定文本的预处理指令。
通常使用 #define 关键字进行定义,在编译前进行简单的文本替换,没有函数调用的开销。
格式如下:
#define 宏函数名(参数列表) 替换文本
例如:Add函数的宏函数实现
#define Add(x,y) ((x)+(y))
其实在我们自己写宏函数的时候比较容易出错,例如上面的Add函数,我们可能会忘记写括号,或者忘记宏函数是怎么定义的。
宏函数的缺点:
- 容易出错,语法坑很多
- 不能调试
- 没有类型安全的检查,在定义和使用宏函数时需要格外小心,避免引发意料之外的错误
- 宏函数在展开过程中可能会导致代码膨胀,增加程序的内存消耗
宏函数的优点
1、没有的类型的严格限制
2、针对频繁调用小函数,不需要再建立栈帧,提高了效率
2.2 内联函数的概念
在C++中,内联函数是一种特殊的函数,以inline修饰,有点类似与宏函数,通常用于执行简单的、频繁调用的代码块,编译时编译器会在调用内联函数的地方展开。
普通的函数使用时直接调用函数↓
使用内联函数之后↓
2.3 内联函数的特性和注意事项
- inline是一种以空间换时间的做法。
- 内联函数的优点:编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
- 缺点:可能使目标文件变大。
- 编译器是否将函数作为内联函数进行优化,是由编译器决定的, inline`关键字只是来提供编译建议,但编译器可以忽略该建议。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有也不需要函数地址了。
- 通常用于执行简单的、频繁调用的代码块。比如一些短小的计算、赋值操作等。
三、auto关键字(C++11)
随着程序越来越复杂,程序中用到的类型也越来越复杂,自己定义的类型名也容易因为含义不明而导致容易使用时出错。
使用typedef给类型取别名确实可以简化代码,但是typedef有时也会在声明变量的时候不能清楚地知道表达式的类型。
3.1 auto关键字的概念
在C++11中,auto关键字的出现是为了简化代码的编写和提高代码的可读性。它的主要应用是自动推导变量的类型。
auto x = 10; // 自动推导为int类型
auto y = 3.14; // 自动推导为double类型
auto z = "Hello"; // 自动推导为const char*类型
3.2 auto的使用细则
- 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型
- auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&int test5() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; cout << x << endl; cout << a << endl; cout << b << endl; cout << c << endl; return 0; }
3.3 auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void test7(auto a) {}
- auto不能直接用来声明数组
void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6}; }
3.4 基于范围的for循环(C++11)
C++11 为我们提供了基于范围的for循环,格式如下:
for循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
int main()
{
int array[] = { 1, 2, 3, 4, 5 ,1, 2, 3, 4, 5 };
for (auto& a : array)
{
a *= 2;
}
for (auto a : array)
{
cout << a << " ";
}
cout << endl;
return 0;
}
范围for的使用条件 :
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定(array是指针形参)void TestFor(int array[]) { for(auto& e : array) cout<< e <<endl; }
- 迭代的对象要能实现++和==的操作。
思考:在早期C语言中也有auto关键字,但遗憾的是一直没有人去使用它,为什么?
答:早期的C/C++中的auto关键字是用于修饰具有自动存储器的局部变量,是默认的存储类说明符,也就是说,如果没有显式地指定存储类说明符,变量会被默认为自动存储器。
此外,还有其他存储类说明符和关键字可供选择,例如static、extern、register等,它们在特定的语境和需求下可能更适用。与auto关键字相比,这些存储类说明符的用法更加直观,并且更容易理解和使用。
综上所述,有其他更直观和易用的选项可供选择,旧的auto关键字在实际编程中逐渐被淘汰和遗忘。
四、指针空值nullptr(C++11)
大家可以猜一下下面的代码结果是多少
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(int)
f(int)
f(int*)
我们通常将NULL当作一个空指针使用,既然是空指针,那也应该是指针类型,那为什么结果是f(int) 呢?
原因:NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强转(void
*)0。
为了解决这个问题,C++11引入了nullptr关键字。nullptr是一种特殊的空指针常量,它可以明确地表示一个空指针,没有类型转换的问题。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(nullptr);//结果是"f(int*)"
return 0;
}
总结:
- 使用nullptr可以提高代码的可读性和安全性。它与指针相关的表达式和操作都可以使用nullptr代替NULL。
- nullptr关键字在比较操作、条件语句、函数调用、类型转换等场景中非常有用。它使得代码更加清晰明了,并有助于避免一些由于空指针引起的错误。