引用
- 什么是引用?
引用是给一个已经存在的变量取一个别名,在语法上并不会给这个别名开一个空间,它和她引用的变量共用一个空间。但是实际上引用也是开了一块空间的,用来存放引用名。引用是按照指针的方式来实现的。 - 引用语法:
类型& 引用变量名(对象名/别称) = 引用实体;注意引用类型必须和被引用类型相同。
int main()
{
int a = 10;
int& b = a;
printf("%p\n", &a);
printf("%p\n", &b);
return 0;
}
- 引用特性:
引用在定义时必须初始化,引用必然是有被引用对象的,不可能说先取个别名,但是不知道这个别名是谁;
一个变量可以有多个引用,相当于一个人可以有很多个外号一样;
引用一旦引用一个实体,就不能再引用其他实体了。
int main()
{
int a = 10;
//int& b;这个会直接报错
int& c = a;
int& d = a;
int e = 20;
c = e;
printf("%p\n%p\n%p\n%p\n", &a, &c, &d, &e);
return 0;
}
如果被引用对象是一个常数,则需要添加const修饰。
int main()
{
const int a = 10;
const int& b = a;
const int& c = 20;
return 0;
}
- 使用场景:
做参数
void swap(int* a, int* b)
{
int x = *a;
*a = *b;
*b = x;
}
void swap(int& a, int& b)
{
int x = a;
a = b;
b = x;
}
int main()
{
int a = 1;
int b = 2;
swap(&a, &b);
cout << a << " " << b << endl;
swap(a, b);
cout << a << " " << b << endl;
return 0;
}
做返回值,可以减少拷贝;调用者可以修改返回值。
需要提前知道一个点,一个函数的返回值实际上是这个值的拷贝,而不是这个值本身。
如果函数返回时,出了函数作用域,如果返回对象还没有还给系统,则可以使用引用返回,如果已经还给系统了,则不能使用引用返回,如果使用结果是返回值是未定义的。
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
- 权限问题:
指针和引用可以进行权限平移和缩小,但是不能进行权限放大。
int main()
{
int a = 1;
const int b = 2;
//权限平移
int& ra = a;
const int& rb = b;
//权限缩小
const int& c = a;
//权限放大
int& d = b;
return 0;
}
- 与指针的区别
引用在概念上是一个变量的别名,指针存储一个变量地址;
引用必须初始化,指针没有规定;
引用在初始化后就不能改变了,指针可以在任何时候改变指向;
在sizeof中含义不同,引用的结果是引用类型的大小,但是指针始终是地址空间所占用的字节个数;
引用自加是被引用的实体加一,指针自加是指针向后偏移一个类型的大小;
指针可以有多级,但引用没有;
访问实体时,指针需要显式的解引用,引用是编译器自己处理;
引用比指针更加安全。
内联函数:
概念:
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,这样就没有 函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。
int Add(int left, int right)
{
return left + right;
}
inline int Sub(int left, int right)
{
return left - right;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
int tmp = 0;
tmp = Sub(4, 3);
return 0;
}
有call说明进行了跳转,是开辟了栈帧空间的。
我们默认的调试模式是在Debug模式,Debug是不会进行代码优化的,如果我们想要看到内联函数的展开就需要修改一些配置。Release又对代码优化的太厉害了,我们不太容易看懂。
特性:
内联函数是一种空间换时间的做法,如果编译器将函数当做内联函数处理,在编译阶段,会用函数体替换函数调用。这是就会出现一个问题,如果内联函数太大,就会使目标文件变大,所以内联函数常被用来写一些短小的函数。实际上,如果内联函数过长,编译器也会忽视这个内联,把它当做普通函数处理;
内联函数对于编译器只是一个建议,不同的编译器关于内联函数的实现机制可能存在不同。在《C++ prime》中明确指出,内联说明只是想编译器发出一个请求,编译器可以选择忽略这个请求;
内联函数的声明和定义一般情况下是不分离的,因为分离之后可能会导致链接错误,内联函数是会被展开的,展开之后就不存在函数地址了,那这个时候去链接地址就会找不到。
使用场景:
内联函数常被用来替代宏函数。这时候就有个问题,既然有宏函数,为什么又要搞出内联函数这个东西?因为宏是较大缺陷的。
宏的优缺点:
优点:增强代码的复用性;提高性能。
缺点:不方便调试(因为在预编译阶段就进行了宏替换);导致代码可读性差,可维护性差,容易误用;没有类型安全检查。
所以在C++中,常使用const enum代替宏的常量定义,用内联函数代替宏函数。
auto关键字(C++11):
在写代码的时候,可能会遇到一个类型名非常长的情况,这种类型名写一两个还行,但是如果多了就非常容易写错。有些人会用typedef来解决这一问题,但是typedef有一个缺陷。
早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但是并没有人使用它。之后在C++11中,auto就具有新的含义了,auto不再是一个存储类型的指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto定义变量时必须初始化,auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器编译时会将auto替换为变量实际的类型。
用autto声明指针类型时,用auto和auto*没有任何区别,但是用auto声明引用类型时必须要加&。
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
auto d = x;
//typeid().name()可以返回变量的类型名
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
printf("%p\n", &x);
printf("%p\n", &c);
printf("%p\n", &d);
return 0;
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数参数;
auto不能直接用来声明数组;
为了避免与C++98中的auto发生混淆,C++11值保留了auto作为类型指示符的用法;
基于范围的for循环(C++):
C++11中引入了基于范围的for循环。for循环后的括号由冒号":"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
int a[] = { 1,2,3,4,5 };
//自动依次取数组中的数据赋值给e(e只是个名字,可以改变),自动判断结束
for (auto e : a)
e *= 2;
for (auto e : a)
cout << e << " ";
cout << endl;
//这里相当于e是a[0]、a[1]等的别名,就可以达到改变数组的效果
for (auto& e : a)
e *= 2;
for (auto e : a)
cout << e << " ";
cout << endl;
return 0;
}
与普通循环类似,也可以使用continue和break;
范围for的循环范围必须是确定的;
指针空值nullptr(C++11):
NULL实际是一个宏定义的,NULL可能被定义为字面常量0,或者被定义为无类型指针(void *)的常量。也不知道出于什么原因被定义成了字面常量0,这样在程序中就会造成一些不可预料的问题。
于是C++11就打了一个补丁,引入nullptr作为关键字,代表指针空值。在C++11中,sizeof(nullptr)与sizeof((void*)0)所占用的字节数相同。