目录
6.引用
6.1引用概念
引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会给其开辟内存空间,它与其所引用的变量共用同一块内存空间。
类型&引用变量名(对象名)=引用实体
6.2引用特性
1.引用在定义时必须初始化
如下便是错误使用
int &b;
2.一个变量可以有多个引用
3.引用只能引用一个实体,即指向不可变,这也是c++中指针不可被替代的重要原因,java中引用指向可变,便无指针。c++中引用和指针并存。
6.3常引用
临时变量具有常性,发生类型转换时,会产生一个临时变量
6.4使用场景
1.做参数
例:
void Swap(int&left,int &right)
{
int temp=left;
left=right;
right=temp;
}
再如
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A aa) {}
void TestFunc2(A& aa) {}
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.做返回值
1)值返回和引用返回比较
在C++中,函数可以通过值返回或引用返回来返回值。
···当使用值返回时,函数将计算结果存储在临时变量中,并将其返回给调用方。在以下示例中,函数返回整数值:
int add(int x, int y) {
int result = x + y;
return result;
}
int z = add(3, 5); // z = 8
···当使用引用返回时,函数会返回对变量的引用,这意味着调用方可以修改该变量。在以下示例中,函数返回对int变量的引用:
int& get(int& x) {
return x;
}
int a = 5;
int& b = get(a);
b = 10; // a = 10
这里函数get()
返回对int
变量的引用,调用方可以使用该引用来修改变量。在上面的代码中,变量a
被传递给get()
函数,然后将返回的引用赋值给变量b
。此时,b
指向变量a
,因此对b
的任何更改都将影响a
。
2)不能使用引用返回的情况
引用返回有以下情况不能使用:
-
函数返回的对象是局部变量:当函数返回一个对局部变量的引用时,当函数执行完毕后该对象将被销毁,这意味着返回的引用将指向一个不存在的对象,从而导致未定义行为。如:
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); cout << "Add(1, 2) is :" << ret << endl; Add(3, 4); cout << "Add(1, 2) is :" << ret << endl; return 0; } //出了函数作用域,返回对象就销毁了,不能使用引用返回,否则结果是不确定
-
函数返回的对象是静态变量:当函数返回一个对静态变量的引用时,该对象将始终存在于程序的生命周期中,因此返回的引用是有效的。但是,这可能会导致多线程问题,在多线程环境中,静态变量可能会被多个线程共享,因此对该变量的引用可能会出现竞争条件。
-
函数返回的对象是临时变量:当函数返回一个对临时变量的引用时,该对象将在函数执行完毕后被销毁,从而导致返回的引用指向不存在的对象。
-
函数返回的对象是常量:当函数返回一个对常量的引用时,该引用不能用于修改对象,因此使用引用返回没有任何好处。
综上所述,当函数返回一个对象的引用时,必须确保该对象的生命周期大于或等于返回的引用的生命周期。
一个静态变量只能初始化一次
3)引用返回相对于值返回的优点
引用返回相比值返回有以下优点:
1.引用返回不会创建副本:当使用值返回时,函数将返回值的副本,对于较大的对象,这可能会导致性能影响。而使用引用返回时,函数将返回对原始对象的引用,因此不会创建副本,因此更加高效。
例如:
// 值返回
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()
{
//TestRefAndValue();
TestReturnByRefOrValue();
return 0;
}
2.引用返回可以修改对象:当使用值返回时,返回的值是一个副本,对该副本的修改不会影响原始对象。而使用引用返回时,将返回对原始对象的引用,因此可以修改原始对象。
例如:
typedef struct SeqList
{
int a[100];
int size;
}SL;
void SLModify(SL* ps, int pos, int x)
{
//...
assert(ps);
assert(pos < ps->size);
ps->a[pos] = x;
}
// 引用做返回值:可以修改返回对象
int& SLat(SL* ps, int pos)
{
assert(ps);
assert(pos < ps->size);
return ps->a[pos];
}
int main()
{
SL s;
//...
SLat(&s, 3) = 10;
// 每个位置的值++
for (size_t i = 0; i < s.size; i++)
{
SLat(&s, i)++;
}
return 0;
}
3.引用返回支持连续赋值:使用引用返回时,可以连续赋值。例如:
int a, b, c;
int& ra = a;
int& rb = b;
int& rc = c;
ra = rb = rc = 0;
在上面的例子中,ra
、rb
和rc
都是对变量a
、b
和c
的引用。因此可以使用连续赋值将它们都设置为0。
总的来说,引用返回可以提高代码的效率和可读性,并且支持一些方便的编程实践,如函数链式调用和赋值。
6.5引用和指针的区别
语法概念上:引用是一个别名,没有独立空间,和引用实体共用同一块空间。
底层实现上:实际上是有空间的,因为引用是按照指针来实现的。汇编代码相同
引用和指针的不同点:
1. 概念:引用在概念上定义一个变量的别名,指针存储一个变量地址。
2. 定义: 引用在定义时必须初始化,指针没有要求。
3. 指向:引用指向不可变,引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
4. 空:没有NUll引用,但有NUll空指针
5. 所占空间大小:在sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数
6. 自加意义不同:引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但没有多级引用
8. 访问实体不同:指针需要显式解引用,引用编译器自己处理。
9. 安全性:引用比指针使用起来相对更安全。
7.内联函数
引入:
c中的宏优点:是不建立栈帧;缺点:1、容易出错,语法细节多 2、不能调试 3、没有类型安全的检查。c++内联函数可作出改进。
概念:
以inline修饰的函数叫做内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,提升程序运行的效率。
相较于宏的优点:
内联函数和宏函数都可以用来提高程序的运行效率,在一些情况下内联函数比宏函数具有一些优点。
-
类型安全:内联函数对于参数类型的要求比较严格,在函数定义时可以进行类型检查,从而避免了一些隐式类型转换带来的问题。而宏函数对于参数类型没有任何限制,容易引起类型错误。
-
调试方便:内联函数对于调试和跟踪程序的执行过程比宏函数更方便。内联函数在被调用时,可以设置断点进行调试,而宏函数的代码是直接展开的,不容易进行调试和跟踪。
-
可读性和可维护性:内联函数相对于宏函数更加易读、易维护和易于理解,因为内联函数的代码可以直接看到,不会像宏函数一样需要展开后才能看到代码。内联函数的代码结构也更加清晰,不容易出现嵌套和逻辑不清等问题。
-
函数特性:内联函数是真正的函数,具有函数的所有特性,例如参数的传递、返回值等。而宏函数只是一段代码的替换,不具备函数的特性,例如参数的传递、返回值等。
总体来说,内联函数相对于宏函数更加稳定、安全,易维护和易于理解。但是,在一些特殊的情况下,宏函数可能会比内联函数更加有效和灵活,例如在对于进行简单的数值计算时。因此,在实际开发中,需要根据具体情况选择合适的函数实现方式。
特性:
1.inline是一种以空间换时间的做法。优点:少了调用开销,提高程序运行效率。缺点:可能会是目标文件变大,这也是为何不全部使用inline的原因。
2.inline相对编译器而言只是一个建议,不同编译器对于inline实现机制可能不同。一般建议:将函数规模较小(即函数不是很长,具体取决于编译器内部实现,10行左右),不是递归,且频繁调用的函数采用inline修饰,否则编译器忽略inline特性。
3.inline不建议声明和定义分离,分离会导致链接错误。简单理解,inline声明与定义若分开,在编译时它不会像普通函数一样在编译时找到定义,因为它是直接展开到所用地,也就是不产生像普通函数一样的函数地址,这一点在反汇编语言中可看到不同。那么分离后便导致链接错误。
// func.h
inline int add(int a, int b) {
return a + b;
}
故一般在头文件中将声明定义放一块(这一点与普通函数不同,普通函数这样做可能会导致同一个定义出现在多个编译单元中,出现链接错误)
8.auto关键字(c++11)
8.1 auto与typedef
typedef
和 auto
都是 C++ 中的类型别名。但是二者主要用途和使用方式有所不同。
typedef
是一种定义自定义类型别名的方式,它可以将一个已经存在的数据类型重命名为另一个名字,使得代码更加简洁易读。例如:
typedef int Integer;
Integer num = 10; // 等同于 int num = 10;
auto
是一种类型推导关键字。它可以在定义变量时省略变量类型的声明,编译器会根据变量初始化表达式自动推导出变量的类型。例如:
auto num = 10; // 推导出 num 的类型为 int
auto
的优点在于:
- 可以使代码更加简洁,特别是对于较长的类型名或模板类型参数。
- 有助于降低代码重构的难度,因为只需要改变初始化表达式的类型即可自动更新变量的类型,而无需手动更改所有的变量声明。
- 可以避免类型定义时重复书写类型名,减少出错的可能性,特别是当类型名较长或较复杂时。
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
8.2auto的使用细则
1.auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但是用auto声明引用类型时则必须加&
原因:这是因为 C++11 标准中规定,使用 auto
推导变量类型时,会忽略初始化表达式的顶层 const 和引用类型,只推导出变量的值类型,因此需要手动加上引用符号。
例:
int a = 10;
int& b = a;
auto c = b; // 编译器推导出 c 的类型为 int,而不是 int&
编译器会忽略初始化表达式 b
中的引用符号,推导出 c
的类型为 int
。如果需要声明一个引用类型的变量来引用变量 b
,则需要加上引用符号:
int a = 10;
int& b = a;
auto& c = b; // 声明一个引用类型的变量 c,引用变量 b
2.在同一行中定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错,因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
auto a = 1,b =2;
auto c = 3,d =4.0;//该行代码会编译失败,因为c和d的初始化表达式类型不同
}
8.3auto不能使用的场景
1.auto不能做函数的参数
2.auto不能直接用来声明数组
9.基于范围的for循环(c++11)
9.1范围for的语法
c++98中如果要遍历一个数组,可以按照以下方式进行
void TestFor()
{
int array[]={1,2,3,4,5}
for(int i=0;i<sizeof(array)/sizeof(array[0]);++i)
array[i]*=3;
for(int*p=array;p<array+sizeof(array)/sizeof(array[0]);++p)
cout<<*p<<endl;
}
对于有范围的集合而言,由程序员本身来说明范围是多余,且有时候还容易出错,c++11中引入基于范围的for循环,for循环后的括号由':'分成两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
9.2范围for的使用条件
1.范围for迭代的范围必须是确定的
对于数组而言,就是数组的第一个元素和最后一个元素的范围;
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
例:for的范围不确定,错误
void TestFor(int array[])//其实这里传过来的本质上是指针,
{
for(auto& e: array)
cout<<e<<endl;
}
2.迭代的对象实现++和--的操作————>使用引用,e++(e--)
10.指针空值nullptr
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器 默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
故引入nullptr表示指针空值
注意:
1.nullptr是c++11作为新关键字引入的。2.在c++11中,sizeof(nullptr)与sizeof((void*)0)所占字节相同,即等价。