六、引用
1).引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
引用的使用方式:
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同种类型的
2).引用的特性
1.引用在定义时必须初始化
int a = 10;
//int& ra; // 需要对引用变量设置初始值
int& ra = a;// 对a取别名ra,两者共用同一块存储空间
2. 一个变量可以有多个引用
int main()
{
int a = 10;
//int& ra; // 需要对引用变量设置初始值
int& ra = a;// 对a取别名ra,两者共用同一块存储空间
int& rb = a;// 可以对一个空间取不同的别名
cout << &ra << endl;
cout << &a << endl;
cout << &rb << endl;
return 0;
}
对上述代码进行运行,可以看到三者的地址相同
3. 引用一旦引用一个实体,再不能引用其他实体
因为引用一旦引用了一个已经存在的实体,就是这个实体的别名,当然不能再成为其他实体的别名。
int main()
{
int a = 10;
int b = 20;
int& ra = a;
int& ra = b;
return 0;
}
3).常引用
我们在创建一个临时变量时,这个变量会存储到栈中,它的权限为可读可写,如果给这个变量加上const后,变量就会被存储到静态区,权限就会变成只读,我们称这种情况为权限的缩小。若此时要对该变量进行引用,那么引用变量也需要被const修饰,const引用变量 引用 const变量,成为const变量的“别名”,实现了权限的平移。
补充:
常见的类型转换:强制类型转换、隐式类型转换、类型截断。
在编辑器进行类型转换时,不是直接将右值直接转换为另一种类型赋值给左值,而是先生成一个临时变量,先将类型转变后的右值赋给临时变量,再将此时的临时变量赋给左值。
只有类型转换才会产生临时变量,临时变量具有常性。
int main()
{
int a = 10;
double c = a;
cout << a << endl;
cout << c << endl;
//double& rc = a;
//只有类型转换才会产生临时变量
//临时变量具有常性
//若进行普通的引用则会导致上述的“权值放大”
const double& rc = a;
// 所以应使用常引用,使“权值平移”
return 0;
}
4).使用场景
1. 做参数
将引用变量作为函数的参数,调用时直接使用变量名,在函数中使用的就是该变量的引用变量,两者共用一块内存空间。
传值、传引用效率比较
#include <time.h>
struct A { int a[10000]; }; // 创建一个较大的数组
// 传值
void TestFunc1(A a) {}
// 引用
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
可以看到引用调用比传值调用效率高
2. 做返回值
将函数的返回值设置引用变量,在函数执行结束后,编译器在返回时会自动生成该返回变量的引用变量并返回,但要注意的是存储返回的变量的空间应该确保不会在函数结束时被销毁,该空间不会被销毁才能保证引用变量的值是准确的。
//出了函数作用域,返回对象就销毁了,不能用引用返回,否则结果是不确定
int& Add(int a, int b)
{
static int c = a + b;
// 加上static后,变量c就会存储到静态区中,在该函数结束后
// 存储变量c的空间就不会被系统自动销毁
// 这样就会避免出现返回值不确定的情况
//局部静态变量成员只能被初始化一次
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
但由于局部静态变量只能被初始化一次,所以在调用Add(3,4)时静态变量c是不会发生变化的若将上述函数改为:
这样变量c就会被修改。
注意:
1.在函数运行时,系统需要给该函数开辟独立的栈空间,用来保存该函数的形参、局部变量以及一些寄存器信息等
2.函数运行结束后,该函数对应的栈空间就被系统回收
3.空间被回收指该块栈空间暂时不能使用,但内存还存在
4.如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
传值返回和传引用返回的效率对比:
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
由上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
5).引用和指针的区别
1. 在语法层面(概念层面)上:引用概念上定义一个变量的别名,指针存储一个变量地址。
2.引用在定义时必须初始化,而指针则没有要求
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同一类实体
4.没有NULL引用,但有NULL指针
5.在sieof中含义不同:
引用结构为引用类型的大小,但指针始终是地址所占字节个数(32位平台下占4个字节,64位平台下占8个字节)
6.引用(++)即引用实体自加1,而指针(++)即指针向后偏移一个类型的大小
7.有多级指针,但没有多级引用
8.访问实体的方式不同,指针需要进行解引用(*),引用直接使用函数名
9.引用比指针使用起来相对安全
两者在底层的实现
由汇编指令可以看出虽然两者在语法层面上不同,但在底层的实现方式是相同的
我们一般在语法层面上理解引用不开辟空间,指针要开辟空间,但实际上引用和指针在底层是一样的都开辟了了空间
结论:引用底层是用指针实现的,但在日常使用中,我们还是以语法层理解,认为引用是不会开辟空间的
7、内联函数
1).内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率。
和宏函数相比内联函数具有宏函数的优点,不用建立栈帧,也克服了宏函数的缺点
宏函数的缺点:
1、当多次使用宏时,宏代码都会插入程序中,除了长度很短的宏。都会明显增加代码长度。
2、计算的结果于预期不符。宏会因为这种运算符的优先级可能导致程序出现不可预料的后果。
#define ADD1(x,y) ( x ) + ( y )
#define ADD2(x,y) (( x ) + ( y ))
int main()
{
int ret1 = ADD1(1, 3) * 5; //(1) + (3) * 5 = 16
int ret2 = ADD2(1, 3) * 5; //((1) + (3)) * 5 = 20
cout << ret1 << endl;
cout << ret2 << endl;
return 0;
}
3、宏是在预编译阶段就把宏和定义的标识符替换了。在运行阶段是无法调试的。
4、没有类型安全的检查。
4、宏不能递归 。
使用内联函数来替代宏函数:
2).内联函数的特性
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运 行效率
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同
一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰
否则编译器会忽略inline特性。
注意:
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址 了,链接就会找不到。
八、 auto关键字(C++11)
1).auto的简介
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
2).auto的使用
auto可以自动识别类型,但使用auto定义变量时必须对其进行初始化。
auto变量省略类型的定义
3).auto的注意事项
1、auto与指针和引用结合起来使用 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
2、当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
3、auto不能作为函数的参数,也最好不要作为函数返回值使用。
4、auto不能直接用来声明数组。
5、 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
6、 auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用。
九 、基于范围的for循环(C++11)
1 ).范围for的语法
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:
第一部分是范 围内用于迭代的变量
第二部分则表示被迭代的范围。
int main()
{
//C语言的方式对数组进行遍历
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
{
cout << *p << endl;
}
cout << endl;
// 依次取数组中元素赋值给e
// 自动判断结束,自动++往后走
for (auto e : array)
{
// 这里的e只是数组元素的临时拷贝,不能对数组元素进行改变
cout << e << " ";
}
cout << endl;
//对数组进行修改
for (auto& e : array)
{
e *= 2;
cout << e << " ";
}
cout << endl;
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2).范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。
十、 指针空值nullptr(C++11)
1). C++98中的指针空值
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。所以我们在C++当中通常使用nullptr来代替空指针。
注意:在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
Test.c
#include<iostream>
#include<assert.h>
using namespace std;
传值返回
//int func()
//{
// int n = 0;
// n++;
// return n;
// //返回n的拷贝,如果返回值较小,则存储到寄存器当中,
// //返回值较大,则需将返回值存储到函数栈帧销毁时无法影响的空间,
// //一般会存储到两个函数栈帧之间
//}
//
//int main()
//{
// int ret = func();
//
// cout << ret << endl;
// cout << ret << endl;
//
// return 0;
//}
//传址返回
//将n的别名返回给ret,但这个值是随机的
//看具体的操作环境
//int& func()
//{
// int n = 0;
// n++;
// return n;
//}
//
//int main()
//{
// int& ret = func();
// cout << ret << endl;
//
// func();
// cout << ret << endl;
// return 0;
//
//}
//int& Count()
//{
// int n = 0;
// n++;
//
// return n;
//}
//
//int main()
//{
// int ret = Count();
// cout << ret << endl;
//
// cout << ret << endl;
//
// return 0;
//}
//int& Count()
//{
// int n = 0;
// n++;
//
// return n;
//}
//
//int main()
//{
// int ret = Count();
//
// cout << ret << endl;
// cout << ret << endl;
//
// /*Count();
// cout << ret << endl;*/
//
// return 0;
//}
//int& Count()
//{
// int n = 0;
// n++;
//
// return n;
//}
//
//int main()
//{
// int& ret = Count();
// cout << ret << endl;
// cout << ret << endl;
// Count();
// cout << ret << endl;
//
// return 0;
//}
//出了函数作用域,返回对象就销毁了,不能用引用返回,否则结果是不确定
//int& Add(int a, int b)
//{
// static int c = a + b;
// // 加上static后,变量c就会存储到静态区中,在该函数结束后
// // 存储变量c的空间就不会被系统自动销毁
// // 这样就会避免出现返回值不确定的情况
// //局部静态变量成员只能被初始化一次
//
// return c;
//}
//int& Add(int a, int b)
//{
// static int c ;
// c = a + b;
// return c;
//}
//int main()
//{
// int& ret = Add(1, 2);
// cout << "Add(1, 2) is :" << ret << endl;
//
// Add(3, 4);
//
// cout << "Add(3, 4) is :" << ret << endl;
// return 0;
//}
//void func1()
//{
// int a = 0;
// cout << &a << endl;
//}
//
//void func2()
//{
// int b = 0;
// cout << &b << endl;
//}
//
//int main()
//{
// func1();
// func2();
//
// return 0;
//
//}
只有类型转换才会产生临时变量
临时变量具有常性
//
//int main()
//{
// int a = 10;
//
// //语法上引用没有开辟空间
// int& b = a;
// //语法上指针是要开辟空间的
// int* ptr = &a;
// //但实际上二者在底层的实现是相同的,都需要开辟空间
//
//
// return 0;
//}
//int main()
//{
// char cmp = 'a';
// char& tcmp = cmp;
//
// cout << sizeof(tcmp) << endl;
// return 0;
//}
宏后面不能加分号 宏是一种替换
//#define ADD(a,b) ((a)+(b))
//
//int main()
//{
// int ret = ADD(3, 4)*5;
// cout << ret << endl;
//
// return 0;
//}
//int main()
//{
// int a = 10;
// int& b = a;//两者需要是同种类型
//
// cout << a << endl;
// cout << b << endl;
//
// return 0;
//}
//int main()
//{
// int a = 10;
// //int& ra; // 需要对引用变量设置初始值
// int& ra = a;// 对a取别名ra,两者共用同一块存储空间
// int& rb = a;// 可以对一个空间取不同的别名
// cout << &ra << endl;
// cout << &a << endl;
// cout << &rb << endl;
//
// return 0;
//}
//int main()
//{
// int a = 10;
// int b = 20;
// int& ra = a;
// int& ra = b;
//
// return 0;
//}
//int main()
//{
// // 权限平移
// //int a = 10; // 存储到栈上
// const int a = 10;// 存储到静态区
// //int& ra = a;
// 因为a为常系数,如果对其直接进行引用,a的权限就会被放大
// 而C++中权限只支持“平移”和“缩小”
//
// //int ra = a;
// 这种情况时创建一个新的空间,将a的值拷贝到新的变量ra当中
//
// const int& ra = a;// 使用常引用对常系数进行引用,可以将其看成“权限的平移”
//
// // 权限缩小
// int c = 20; // 此时,可以对c进行修改
//
// const int& rc = c; // 使用常引用对c进行引用,将c的权限变成只读,可以将其看成“权限的缩小”
//
// // 对常数进行常引用
// const int& d = 10;
//
// return 0;
//}
//int main()
//{
// int a = 10;
// double c = a;
// cout << a << endl;
// cout << c << endl;
//
// //double& rc = a;
// //只有类型转换才会产生临时变量
// //临时变量具有常性
// //若进行普通的引用则会导致上述的“权值放大”
//
// const double& rc = a;
// // 所以应使用常引用,使“权值平移”
//
// cout << a << endl;
// cout << &rc << endl;
//
// return 0;
//}
指针
//void swap(int* x, int* y)
//{
// int tmp = *x;
// *x = *y;
// *y = tmp;
//}
//
引用
//void swap(int& x, int& y)
//{
// int tmp = x;
// x = y;
// y = tmp;
//}
//
//int main()
//{
// int a = 10;
// int b = 20;
// int c = 30;
// //指针
// swap(&a, &b);
// cout << "a =" << a << endl;
// cout << "b =" << b << endl;
//
// swap(b, c);
// cout << "b =" << b << endl;
// cout << "c =" << c << endl;
//
// return 0;
//}
//#include <time.h>
//struct A { int a[10000]; }; // 创建一个较大的数组
传值
//void TestFunc1(A a) {}
引用
//void TestFunc2(A& a) {}
//
//void TestRefAndValue()
//{
// A a;
// // 以值作为函数参数
// size_t begin1 = clock();
// for (size_t i = 0; i < 10000; ++i)
// TestFunc1(a);
// size_t end1 = clock();
// // 以引用作为函数参数
// size_t begin2 = clock();
// for (size_t i = 0; i < 10000; ++i)
// TestFunc2(a);
// size_t end2 = clock();
// // 分别计算两个函数运行结束后的时间
// cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
// cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
//}
//#include <time.h>
//struct A { int a[10000]; };
//A a;
值返回
//A TestFunc1() { return a; }
引用返回
//A& TestFunc2() { return a; }
//void TestReturnByRefOrValue()
//{
// // 以值作为函数的返回值类型
// size_t begin1 = clock();
// for (size_t i = 0; i < 100000; ++i)
// TestFunc1();
// size_t end1 = clock();
// // 以引用作为函数的返回值类型
// size_t begin2 = clock();
// for (size_t i = 0; i < 100000; ++i)
// TestFunc2();
// size_t end2 = clock();
// // 计算两个函数运算完成之后的时间
// cout << "TestFunc1 time:" << end1 - begin1 << endl;
// cout << "TestFunc2 time:" << end2 - begin2 << endl;
//}
//
//int main()
//{
// TestReturnByRefOrValue();
// return 0;
//}
//int main()
//{
// int a = 10;
//
// int& ra = a;
// int* ptr = &a;
//
// return 0;
//}
//#define ADD1(x,y) ( x ) + ( y )
//#define ADD2(x,y) (( x ) + ( y ))
//
//int main()
//{
// int ret1 = ADD1(1, 3) * 5; //(1) + (3) * 5 = 16
// int ret2 = ADD2(1, 3) * 5; //((1) + (3)) * 5 = 20
//
// cout << ret1 << endl;
// cout << ret2 << endl;
//
// return 0;
//}
在C++当中我们通常使用
enum const 来替换宏常量
inline 来替换宏函数
//inline int ADD(int x, int y)
//{
// int c = x + y;
// return c * 5;
//}
//
//int main()
//{
// int ret = ADD(1, 3);
// // 直接将内联函数展开,
// //不使用call指令通过函数地址调用函数
// cout << ret << endl;
//
// return 0;
//}
//int TestAuto()
//{
// return 10;
//}
//
//int main()
//{
// int a = 10;
// auto b = a;
// auto c = 'a';
// auto d = TestAuto();
//
// cout << typeid(b).name() << endl;
// cout << typeid(c).name() << endl;
// cout << typeid(d).name() << endl;
// //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
// return 0;
//}
//int main()
//{
// int a = 0;
// int b = a;
// auto c = a;
// auto d = &a;
// auto* e = &a;
// auto& f = a;
// // 引用变量的类型是int
// // 在语法上 f 是 a 的别名,因为a的类型是int
// // 所以f的类型也应该为int
// f++;
//
//
// cout << typeid(c).name() << endl;
// cout << typeid(d).name() << endl;
// cout << typeid(e).name() << endl;
// cout << typeid(f).name() << endl;
//
// return 0;
//}
//#include<vector>
//#include<string>
//int main()
//{
// // auto意义定义对象时,类型较长,用它比较方便
// vector<string> v;
// vector<string>::iterator it = v.begin();// 不使用auto
//
// // 使用auto自动识别函数类型
// auto it = v.begin();
//
// // 不可以,需要对x进行初始化
// //auto x;
//
//
// return 0;
//}
//int main()
//{
// //C语言的方式对数组进行遍历
// int array[] = { 1, 2, 3, 4, 5 };
// for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
// {
// array[i] *= 2;
// }
//
// for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
// {
// cout << *p << endl;
// }
// cout << endl;
//
// // 依次取数组中元素赋值给e
// // 自动判断结束,自动++往后走
// for (auto e : array)
// {
// // 这里的e只是数组元素的临时拷贝,不能对数组元素进行改变
// cout << e << " ";
// }
// cout << endl;
//
// //对数组进行修改
// for (auto& e : array)
// {
// e *= 2;
// cout << e << " ";
// }
// cout << endl;
//
// return 0;
//}
//void TestFor(int array[])
//{
// // 这里实际只有数组arr的首元素地址
// // 是一个指针,无法使用范围for
// for (auto& e : array)
// {
// cout << e << endl;
// }
//}
//
//int msin()
//{
// int arr[] = { 1,2,3,4,5,6,7,8,9 };
// TestFor(arr);
//
// return 0;
//}
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
// NULL可能被定义为字面常量0,
// 或者被定义为无类型指针(void*)的常量。
// 所以我们在C++当中通常使用nullptr来代替空指针
f(nullptr);
return 0;
}