文章目录
一、引用
1、概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。这就像小时候家里给起的小名,例如:小明,这与大名指的是同一个实体,共用一个空间。
类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}注意:引用类型必须和引用实体是同种类型
2、特性
- 引用在定义时必须初始化。
- 一个变量可以有多个引用。
- 引用一旦引用一个实体,就不能引用其他实体。
void TestRef()
{
int a = 10;
// int& ra; //没有初始化,所以编译器会报错
int& ra = a;
int& rra = a;//与ra引用的是同一个实体
printf("%p %p %p\n", &a, &ra, &rra);
}
3、常引用
目的:不想通过引用来改变原来空间的值
形式: const int &c = a //相当于 const int * const c = &a
常引用初始化有两种情况:
- 用变量 初始化常引用
int e = 30;
const int &f = e;//用e变量去初始化 常引用- 用字面量 去初始化 常量引用,不能用常量给引用赋值
const int g = 40;
//int &rg=g; 报错哦!
const & rg=g;可以哦!
//int &m = 41;//普通引用 引用一个字面量 字面量中没有内存地址 不可以哦
const int &m = 43;//c++编译器会分配内存空间 给&m
cout <<"m="<<m << endl; //输出为43

4、使用场景
- 做函数的参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}- 做函数的返回值
int& TestRefReturn(int& a)
{
a += 10;
return a;
}如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。像下面这个例子就会出问题:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);//用的栈上的空间进行返回,函数结束时就会返还栈上的空间。
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;//结果是Add(1,2)is:7
return 0;
}5、引用和指针
- 传值、传引用效率比较:
#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 << "值-time:" << end1 - begin1 << endl;
cout << "引用-time:" << end2 - begin2 << endl;
}
// 运行多次,检测值和引用在传参方面的效率区别
int main()
{
for (int i = 0; i < 10; ++i)
{
TestRefAndValue();
}
system("pause");
}上面代码运行结果:
值-time:16 引用-time:1
值-time:16 引用-time:0
值-time:16 引用-time:0
值-time:15 引用-time:1
值-time:14 引用-time:0
值-time:14 引用-time:0
值-time:13 引用-time:1
值-time:14 引用-time:1
值-time:14 引用-time:0
值-time:15 引用-time:0
我们可以看到传值的效率比传引用的效率低了不少!
- 引用和指针的相同点
引用:在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
指针:在底层是有空间的。
这是指针和引用的汇编代码:
我们可以看出,在底层引用就是按照指针的方式来实现的。
- 引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
二、内联函数
1.概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。适用于功能简单,规模较小又使用频繁的函数。但是无法处理有递归的函数,内联函数不能有循环体,switch语句,不能进行异常接口声明。
这是普通函数调用时的汇编代码:

这是加入inline后内联函数的汇编代码

从中我们可以看到内联函数省去了函数调用的额外开销,这其实是一种以空间换时间的做法,所以代码很长或者有循环或递归函数就不适合用内联函数了。
2、特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}//链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用3、宏函数和内联函数的区别
首先先来介绍一下什么是宏:宏定义又称为宏代换、宏替换,简称“宏”。
格式:
#define 标识符(大写) 字符串
其中标识符就是所谓的符号常量,也称为“宏名”。除了一般的字符串替换,还要做参数代换
格式: #define 宏名(参数表) 字符串
#define Add (a,b) a+bret=Add(3,2); //第一步被换为Add=a+b;
//第二步被换为ret=3+2;
#define Add (a,b) a+b
area=Add(a+b);//第一步换为area=r+r;
//第二步被换为area=a+b+a+b;说明:
(1)宏名一般用大写;
(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。
例如:数组大小常用宏定义;
(3)宏的哑实结合不存在类型,也没有类型转换。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头;
(6)可以用#undef命令终止宏定义的作用域;
(7)宏定义允许嵌套;
(8)字符串( ” ” )中永远不包含宏;
(9)宏定义不分配内存,变量定义分配内存;
(10)宏定义不存在类型问题,它的参数也是无类型的。
宏和函数的区别
1.函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
2.宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。
3.宏展开使源程序变长,函数调用不会。
宏函数相比普通函数
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
宏和内联函数的区别
1.内联函数采用的是值传递,而宏定义采用的是对等替换.
2.宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销
3.编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
4、思考
C++有哪些技术替代宏?
常量定义 换用const
函数定义 换用内联函数
类型重定义
#defineDWORD unsigned int这种类型重定义完全可以使用 typedef unsigned int DWORD 来替代。条件编译
#ifdefSystemA testA(); #else//SystemB testB(); #endif头文件包含
#ifndeftest_h #definetest_h //test.h的实现 #endif
三、auto关键字
1、概念
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
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;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
2、auto的使用方法
- auto和指针和引用结合
用auto声明指针类型时候,用auto和auto*没什么区别,但是用auto声明引用的时候,必须加&。
int main()
{
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;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能推导的场景:
- auto不能作为函数的参数
- auto不能来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
进行配合使用。 - auto不能定义类的非静态成员变量
- 实例化模板时不能使用auto作为模板参数
四、范围for循环
这个和auto一样,都是C++11的语法,以前我们遍历一个数组的时候是这样做的:
void TestFor()
{
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;
}而在C++11中用了基于for循环的语法来遍历一个数组就很方便。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array) e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
- for循环迭代的范围是确定的数组,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定产生错误,所以不能使用for循环迭代
void TestFor(int array[])//数组大小不知道
{
for(auto& e : array)
cout<< e <<endl;
}五、空值指针nullptr
这个也是C++11中的语法,在C++98中,我们经常使用NULL来给一个指针赋空,但是NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。这样子会有一些麻烦的问题:
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(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
nullptr 与 nullptr_t
为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了nullptr,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
以上就是C++学习之前我们需要先掌握的一些知识,这些知识在我们后面写C++代码的时候会经常用到,所以这些知识需要了熟于心,为后面的面向对象编程打一个良好的基础!