C++基础(二)

引用

引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它
引用的变量共用同一块内存空间 。
类型& 引用变量名(对象名) = 引用实体

void TestRef()
{
    int a = 1;
    int& b = a;           //<===定义引用类型

    cout << &a << endl;   //取地址
    cout << &b << endl;  //取地址

    a++;
    cout << a << endl;

    b++;

    cout << b << endl;


    cout << &a << endl;   //取地址
    cout << &b << endl;  //取地址
}

image.png

引用特性

  1. 引用在定义时必须初始化

  2. 一个变量可以有多个引用

  3. 引用一旦引用一个实体,再不能引用其他实体

    int main()
    {
     int a = 1;   // 1.引用在定义时必须初始化
     //int& b;  此时编译会出错
    
     int& b = a;  // 2.一个变量可以有多个引用
     int& c = a;
     int& d = c;
    
     int x = 10;  // 3.引用一旦引用一个实体,再不能引用其他实体
     b = x;        //这里表示x赋值给b
    
    

}

![image.png](https://cdn.nlark.com/yuque/0/2022/png/29339358/1660878173404-07d355a1-5b94-4408-a357-1f7f2c1d0904.png#clientId=ub865491e-9337-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=378&id=u18cc9dfe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=473&originWidth=1355&originalType=binary&ratio=1&rotation=0&showTitle=false&size=55981&status=done&style=none&taskId=u266435d2-c8e1-4afe-8ad3-b7d588b91c8&title=&width=1084)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/29339358/1660878266586-9178a912-abdd-485a-8d35-9bb8df4cd8f2.png#clientId=ub865491e-9337-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=506&id=u06dc17c5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=632&originWidth=1427&originalType=binary&ratio=1&rotation=0&showTitle=false&size=75160&status=done&style=none&taskId=ua3cd172b-5a5d-498d-af47-de11da568ee&title=&width=1141.6)
<a name="wH90L"></a>
### 使用场景
<a name="QdH4A"></a>
#### 1.做参数
a.输出型参数
```cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}

另一个地方是在数据结构学习链表的时候,我们可以不用传一级指针,直接用引用就可以,改变引用同时也改变了实体


b.大对象传参,提高效率

#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.做返回值

a.做输出型返回对象,调用者可以修改返回对象
b.减少拷贝,大对象返回提高效率

int& Count()
{
static int n = 0;
n++;
// ...
return n;
}

传引用返回(错误示例)
返回的时n的别名,这里出现了越界,但编译器没有报错,仍然会运行。出了函数栈帧后,系统会将其清理成随机值,这里ret仍然可以输出1的结果属于一种侥幸。image.png
image.png
这里使用引用返回本身是错误的,得出的结果是没有保障的

image.png
若在ret前加个引用,此时ret和tmp都是Count这块空间的别名,第一次调用printf函数,需要进行传参数,此时传的是ret的参数,能够找到并返回1。在传参调用的过程中也需要建立栈帧,此时系统会编写随机值将n的值覆盖掉,当再一次调用printf函数时,就无法找到并返回1。总的来说这种引用返回是错误的,因为出了栈帧后这块空间就不属于Count了。得到的结果并不可靠


传引用返回(正确示例)
image.png
这里是链表的修改

typedef struct SeqList
{
    int* a;
    int size;
    int capacity;
}SL;


void SLInit(SL& s, int capacity)
{
    s.a = (int*)malloc(sizeof(int) * capacity);
    assert(s.a);
    // ...
    s.size = 0;
    s.capacity = capacity;
}

void SLPushBack(SL& s, int x)
{
    if(s.size == s.capacity)
    {
        // ...
    }

    s.a[s.size++] = x;
}

int& SLAt(SL& s, int pos)
{
    assert(pos >= 0 && pos <= s.size);

    return s.a[pos];
}






int main()
{
    SL sl;
    SLInit(sl);
    SLPushBack(sl, 1);
    SLPushBack(sl, 2);
    SLPushBack(sl, 3);
    SLPushBack(sl, 4);

    for (int i = 0; i < sl.size; ++i)
    {
        cout << SLAt(sl, i) << " ";
    }
    cout << endl;

    SLAt(sl, 0)++;

    for (int i = 0; i < sl.size; ++i)
    {
        cout << SLAt(sl, i) << " ";
    }
    cout << endl;

    SLAt(sl, 0) = 10;

    for (int i = 0; i < sl.size; ++i)
    {
        cout << SLAt(sl, i) << " ";
    }
    cout << endl;


    return 0;
}

传值返回
当n为局部变量时:
image.png

当n被static修饰时
image.png

因为n为局部变量,出了Count这个栈帧后就会销毁。所以会存一个临时变量来储存n的值,当这个值较小时,一般会放在寄存器里面(大小为4个字节或8个字节)。当较大时,会提前在main函数栈帧中提前开辟一个空间进行存储。只要是传值返回,无论n为何种变量,都不会把n本身传过去,都是通过生成一个返回对象拷贝作为函数调用返回值。

总结:只要出了函数作用域,此时返回对象已经被销毁了,就不能用传引用返回。要用传值返回。上面使用static时,可以用传引用返回

常引用

这里就牵扯到权限的相关问题

int main()
{

    //权限平移
    int a = 1;
    int& b = a;

    cout << typeid(a).name()<< endl;
    cout << typeid(b).name() << endl;


    //权限放大  错误的
    const int c = 1;
    // int& d = c;



     //权限缩小
    int e = 10;
    const int& f = e;

    return 0;


    //隐式类型转换
    int ii = 1;
    double dd = ii;        //强转并不会改变原变量的类型
    //double& edd = ii;   不可以,本质上发生了权限的放大,因为临时变量具有常性
    const double& rdd = ii;

   //引用常量
    const int& x = 10;

}

图中的rdd是临时变量的别名。 权限只可以平移或缩小,不能放大(const是只读取不修改)。权限的放大缩小只针对引用和指针
image.png
包括刚刚的引用、之前C语言中的强制类型转换、提升、截断等:char ch = ‘A’; int i = ch;这中间也会产生临时变量,对临时变量进行一系列操作后(类型改变,原反补计算等),将值赋给i

const类型具有很强的接受度,从上面我们可以看出,它可以接收变量,常量,临时变量等,所以如果使用引用传参,函数内部不改变参数,那么简易用const引用传参
image.png

引用和指针的区别

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
    实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
    4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

下面我们通过反汇编来观察
image.png
从语法角度上来看,引用好像没有开辟新的空间,但实际上底层实现和指针一样,都是通过存储a的地址,然后进行访问。

内联函数

概念

在我们编译代码的过程中,遇到那些短小的函数(1-10行),并且需要频繁调用时,利用C语言知识我们会用到宏函数
宏函数的优点: 1.宏函数复用性强 2.宏函数提高效率,减少栈帧的建立
宏函数的缺点: 1.可读性变差 2.没有类型安全检查 3.不方便调试
**#define ADD(a,b) ((a) + (b))**


对此在c++的学习中,我们引出inline
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧
的开销,内联函数提升程序运行的效率。
image.png
image.png
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进
    行优化,以下给出vs2013的设置方式)

image.png


特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替
    换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率

image.png

  1. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规
    模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数
    采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议

  2. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会
    找不到。

image.png

** C++有哪些技术替代宏?**

  1. 常量定义 换用const enum
  2. 短小函数定义 换用内联函数

## auto关键字 auto不再是一个存储类型指示符,而是作为一个新的类型
指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
### 可以使用的情况 类型比较长的时候,auto自动推导 #### 1.搭配for循环使用 for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,
第二部分则表示被**迭代的范围(**对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 **) **。 与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
```cpp

int main()
{
int a[] = { 0,1,2,3,4 };
//范围for
// 自动依次取a的数据,赋值给e
// 自动迭代,自动判断结束
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
}

注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类<br />型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为<br />变量实际的类型。 <br /> 
<a name="B0uWr"></a>
#### 2.auto与指针和引用结合
<br />用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加& <br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/29339358/1661074520890-a22a2110-f8ca-481d-8907-a1c68ad28d39.png#clientId=u974b636e-499a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=242&id=u3caf0381&margin=%5Bobject%20Object%5D&name=image.png&originHeight=303&originWidth=1428&originalType=binary&ratio=1&rotation=0&showTitle=false&size=61006&status=done&style=none&taskId=ua9723e89-c0e3-41df-a8ed-d80a951d97c&title=&width=1142.4)
```cpp
int main()
{
    int x = 10;
    auto a = &x;  // int*
    auto* b = &x;  // int* 强调一定要传指针
    auto& c = x;   // int 强调c是一个引用
    cout << typeid(a).name() << endl;
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;

    return 0;
}


3.在同一行定义多个变量


当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量

void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

不可以使用的情况

1.auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2.auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {456};
}


指针空值nullptr(C++11)

之前学过的C语言里面我们所使用的空指针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()
{
    int *p = NULL;
    f(0);
    f(NULL);          //这里被替换为0
    f(p);             //这里NULL替换为0,0又被p替换,p的类型是int*
return 0;
}

image.png

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下
将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

裙下的霸气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值