C++入门(下)

一、引用

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
我们可以看到传值的效率比传引用的效率低了不少!

  • 引用和指针的相同点
    引用:在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
    指针:在底层是有空间的。

这是指针和引用的汇编代码:
在这里插入图片描述
我们可以看出,在底层引用就是按照指针的方式来实现的。

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

二、内联函数

1.概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。适用于功能简单,规模较小又使用频繁的函数。但是无法处理有递归的函数,内联函数不能有循环体,switch语句,不能进行异常接口声明。

  • 这是普通函数调用时的汇编代码:
    在这里插入图片描述

  • 这是加入inline后内联函数的汇编代码
    在这里插入图片描述
    从中我们可以看到内联函数省去了函数调用的额外开销,这其实是一种以空间换时间的做法,所以代码很长或者有循环或递归函数就不适合用内联函数了。

2、特性

  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  3. 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+b
ret=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++有哪些技术替代宏?

  1. 常量定义 换用const

  2. 函数定义 换用内联函数

  3. 类型重定义
    #defineDWORD unsigned int这种类型重定义完全可以使用 typedef unsigned int DWORD 来替代。

  4. 条件编译

    #ifdefSystemA
    testA();
    #else//SystemB
    testB();
    #endif
  5. 头文件包含

    #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的使用方法

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

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

auto不能推导的场景:

  1. auto不能作为函数的参数
  2. auto不能来声明数组
  3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
  4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
    进行配合使用。
  5. auto不能定义类的非静态成员变量
  6. 实例化模板时不能使用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的使用条件

  1. 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;

注意:

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

以上就是C++学习之前我们需要先掌握的一些知识,这些知识在我们后面写C++代码的时候会经常用到,所以这些知识需要了熟于心,为后面的面向对象编程打一个良好的基础!


版权声明:本文为qq_40704847原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。