c++关键字,命名空间,缺省参数,函数重载,引用,内联函数auto等学习(下)

简介: c++关键字,命名空间,缺省参数,函数重载,引用,内联函数auto等学习(下)

六:引用



c++中的引用非常好用,可以避免我们在c语言中使用一级指针二级指针等等,引用不是重新定义一个变量,而是给这个变量起一个别名,他和他引用的变量在同一个空间地址。


7f2aacb4ab4e441184c3663b2228feb0.png


如图所示k是a的引用,在我们查看a和k的地址的时候发现他们俩的地址相同,说明他们在同一块空间。在这里要说明一点,引用类型一定是和引用实体是同种类型的。


7f4eca3df5c849d1963405c574e9fe89.png


在a自加后k也自加了也能证明k就是a。

40419a82926548d88fe271b083b4511e.png

以前需要传地址才能交换两个数现在直接用引用就能解决。


引用的特性:


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

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

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


eb0b5d80b1764767b5c94ef1c83a43a4.png


因为b已经是a的引用了,然后b又去当c的引用所以就报错了。


引用使用都有什么场景呢?第一个场景就是刚刚swap函数中用引用做参数,第二个场景就是用引用做返回值。


用引用做返回值有什么好处呢?我们学过C语言的都知道当函数结束需要返回的时候会创建一个临时变量去接收返回值然后销毁函数栈帧,那么在创建临时变量的过程中无疑会浪费空间,我们发现当一个变量是静态的或者是函数结束不被销毁的,那么我们就可以用引用返回这样就避免了空间的浪费。


#include <assert.h>
#define N 10
typedef struct Array
{
  int a[N];
  int size;
}AY;
int& PosAt(AY& ay, int i)
{
  assert(i < N);
  return ay.a[i];
}
int main()
{
  Array ay;
  for (int i = 0; i < N; i++)
  {
  PosAt(ay, i) = 10 * i;
  }
  for (int i = 0; i < N; i++)
  {
  cout << PosAt(ay, i) << " ";
  }
  cout << endl;
  return 0;
}


fe3b014f13da4cbd9de1eadd5f1f87dd.png


从上图中我们可以看到ay是我们创建的结构体对象,这个对象在函数结束才会销毁,那么像以前C语言那样每次函数返回一个值就需要开一个临时变量去接收返回值很浪费空间,既然这个变量在函数调用后没有被销毁那么就可以直接返回自己,所以在上图中我们使用了引用做返回值。


用引用返回有两个特点:


1.减少拷贝                                            2.调用者可以修改返回对象


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;
    return 0;
}

上面这个代码有什么问题呢?这个代码是正确的吗?很明显这个代码是错误的,我们已经说过引用返回仅限于函数栈帧销毁后还存在的变量,add的返回值c作用域仅限于add函数当add函数返回就被销毁了,那么这个时候c的空间是不能被访问的。


如果函数返回时,出了函数作用域,如果返回对象还在 ( 还没还给系统 ) ,则可以使用

引用返回,如果已经还给系统了,则必须使用传值返回。


常引用:

c6cff5cb3e584b7a86cf3425d63a8e59.png


如图所示我们发现好像不能去引用const修饰的变量,这是为什么呢?

82f274a8f1814cbcad34680335b410eb.png



这是因为指针和引用,在赋值/初始化的时候权限只能缩小,不能放大,本来a的权限仅仅是只读,结果在引用的时候给了aa可读可写的权限,这当然是不可以的。


int main()
{
  int c = 1;
  int& cc = c;
  //权限的放大
  //const int a = 10;
    //int& aa = a;   //权限的放大会报错
  // const int*ptr = NULL
  // int* pptr = ptr
  //权限的缩小
  int a = 10;
  const int& aa = a;   //权限的缩小没有问题
  int* p = NULL;
  const int* p1 = p;
  return 0;
}


b6795993e0d7451f9fcd0ea393b7990e.png


上图中为什么会报错呢?我们可以看到count函数的返回类型为传值返回,传值返回返回的是临时变量,由于临时变量具有常性,所以我们必须加上const


int count()
{
  int n = 0;
  n++;
  return n;
}
int main()
{
  const int& ret = count();
  return 0;
}


d1794be90c174e5e9bf8908519934777.png

上图中的代码怎么修改才是正确的呢?因为从int转换成double需要隐式转换,而类型转换会产生临时变量,还是刚刚的问题临时变量具有常性所以加上const就可以了


int main()
{
  int i = 0;
  const double& rb = i;
  return 0;
}


引用和指针的区别:


在语法概念上引用就是变量的别名,没有独立空间,和其引用实体共用一块空间。


而在底层实现上实际上是有空间的,因为引用是按照指针的方式实现的。


int main()
{
  int a = 10;
  int& ra = a;
  ra = 20;
  int* pa = &a;
  *pa = 20;
  return 0;
}

aedfb9ad1d8c46dfa28ef00e681a36ee.png


从上图我们可以看到int& ra = a  和  int* pa = &a 的反汇编实现是一样的,这也可以证明引用是按照指针的方式实现的。


引用和指针的不同点:


1.引用概念上是定义一个变量的别名,指针存储一个变量的地址


2.引用在定义时必须初始化,而指针可以不初始化。


3.引用在初始化引用一个实体后就不能再去引用其他的实体,而指针可以在任何时候指向任何一个同类型实体。


4.没有空引用,但是有NULL指针。


5.在sizeof中的含义不同,引用的大小是其引用实体类型的大小,而指针永远是32位下4字节,64位下8字节


6.引用自加就是其引用实体自加,而指针自加是指针往后偏移一个类型的大小。


7.有多级指针,但是没有多级引用。


8.访问实体的方式不同,指针需要显示解引用,引用由编译器自动处理。


9.引用比指针使用起来相对更安全


七:内联函数



在c++中一般不在用宏了,一般都用const和enum去替代宏常量,用inline去替代宏函数。


那么为什么c++中不使用宏了呢?因为宏的缺点很明显,第一:不能调试。第二:没有类型安全的检查。第三:有些场景下非常复杂。


为什么说非常复杂呢?大家可以现在用宏写一个ADD函数


#define Add(x,y) ((x)+(y))
int main()
{
  //如果将宏定义成这样#define Add(x,y) (x)+(y)
  int ret = Add(10, 15) * 3;   //结果为(10)+(15)*3 与我们想的(10+15)*3就不一样了
  int a = 1, b = 3;
  //如果将宏定义成#define Add(x,y) x + y
  int ad = Add(a & b, a | b);  //结果为 a & (b+a) | b,因为+的优先级高于&和|所以会先进行+
  return 0;
}

以inline修饰的函数叫内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。看到这里大家也发现了这不就是宏的优点吗,既然宏的优点被替代了那么自然就很少再使用宏了。


下面是内联函数的反汇编:


inline int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int ret = Add(1, 2);
  cout << "ret:" << ret << endl;
  return 0;
}

9a0f3af6752644e3a85880cd9718d1f9.png


下面是普通函数的反汇编:


int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int ret = Add(1, 2);
  cout << "ret:" << ret << endl;
  return 0;
}


c3b2c604b6994ea9b0df95cec0903763.png

通过对比我们发现内联函数在汇编中直接展开,不会再像普通函数那样开一个函数栈帧进入这个函数。


内联函数的特性:


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


2.inline对于编译器而言只是一个建议,不同编译器关于inline的实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归且频繁调用的函数采用inline修饰,否则编译器会忽视inline特性


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


八:auto关键字



使用auto关键字可以让编译器自动推导其类型。


int main()
{
  int a = 0;
  auto b = a;
  auto c = &a;
  cout << typeid(b).name() << endl;
  cout << typeid(c).name() << endl;
  return 0;
}

用typeid().name()可以查看auto推导的是什么类型。


6d2cc8a4606a49fb90034937bc8a4248.png


auto的实际价值:简化代码,当类型很长的时候,可以考虑自动推导。


在这里就会有人说了,typedef不是也可以起到简化代码的作用吗?可以是可以,但是typedef在一些场景下会有很大的缺点,比如:


typedef char* pstring;
int main()
{
  const pstring p1;
  const pstring* p2;
  return 0;
}

大家可以看一下上面的代码哪条会报错呢?


af228ca204794de7810a672a86906567.png


答案是p1,这就让人很疑惑了,为什么const char* p1会出错呢?出错的原因在于使用typedef重命名char*后,p1实际上变成了char* const p1,const去修饰p1很明显p1变成了一个常量,常量的定义必须初始化。这就是typedef的缺点。

428f8b2cb64e495daa4858e68a502f81.png

我们在使用auto的时候,可以强制类型,比如:


int main()
{
  int a = 10;
  auto* aa = &a;   //强制aa是指针类型,当然不加*编译器也能自己推导出来aa的类型
  //auto aa = &a;
  char c = 'a';
  auto& d = c;  //auto后想要其是另一个变量的引用必须加上引用符号
  cout << typeid(aa).name() << endl;
  cout << typeid(d).name() << endl;
  return 0;
}

f87526440bab4842b03487c98e024111.png

注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的“占位符”,编译器会在编译期会将auto替换为变量实际的类型。


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

0812ded9f8f44131aa3864a945ccbb04.png


正确的应该是:


int main()
{
  auto a = 10, b = 20;
  auto c = 2.33, d = 2.20;
  return 0;
}

auto不能推导的场景:


1.auto不能作为函数的参数

874deed7052f4d39a1e47b11696e047a.png


2.auto不能直接用来声明数组

6536c3894a5f4ddc80eb536f28f1e008.png


九:基于范围的for循环



对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误,因此c++11中引入了基于范围的for循环。for循环后的括号由冒号“ : ”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,如下图所示:


int main()
{
  int array[] = { 1,6,7,4,2,9,5 };
    //自动依次取数组中数据赋值给e对象,自动判断结束
  for (auto e : array)
  {
  cout << e << " ";
  }
  cout << endl;
  return 0;
}

f3f8d8c0ff054e7694d191856e763307.png

如果赋值给数组中的元素直接用e赋值即可


int main()
{
  int array[] = { 1,6,7,4,2,9,5 };
  for (auto e : array)
  {
  e *= 2;
  cout << e << " ";
  }
  cout << endl;
  for (auto e : array)
  {
  cout << e << " ";
  }
  cout << endl;
  return 0;
}

58c5db16ff9e4c488ab77a0d1731c790.png


通过上图我们可以发现赋值后好像并没有改变原数组,这该怎么办呢?其实很简单,我们定义迭代的变量的使用用引用即可。


c83531740a0846e4a7e49e8c6b7d893e.png


范围for的使用条件:


for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围。


如下图所示,这样的代码就不能使用范围for:

4b2476a066b046dea836477439ebd2ef.png


我们在学习C语言的时候就知道,数组传参只是数组首元素地址,是不知道数组有多少个元素的,需要将数组内的元素大小也传过来。


十:指针空值nullptr



c++中的nullptr实际上是打的一个补丁,因为c++中的NULL出了bug,如下图所示:


void f(int)
{
  cout << "f(int)" << endl;
}
void f(int*)
{
  cout << "f(int*)" << endl;
}
int main()
{
  f(0);
  f(NULL);
  return 0;
}

按照我们所想f(0)应该调用的第一个f函数,f(NULL)调用的应该是传指针的那个函数,但是事实却并不是这样。

5b9d938ee548448dbf734056592a83a5.png


我们可以看到都调用了f(int)这个函数,这是因为在C中NULL实际上是一个宏,如下图:

80a780e8d2cc424a9052171b23066f4a.png


我们可以看到在c++中NULL被定义为0.在c++11中打了一个补丁加了一个关键字nullptr,nullptr是能正确使用的。


void f(int)
{
  cout << "f(int)" << endl;
}
void f(int*)
{
  cout << "f(int*)" << endl;
}
int main()
{
  f(0);
  f(NULL);
  f(nullptr);
  return 0;
}

d835669e589c4208b629e24bcee8d59d.png


如上图所示,nullptr正确匹配了f函数。


注意:


1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是c++11作为新关键字引入的。

2.在c++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。

3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


总结



本次所讲解的都是从c过渡到c++所改进的一些东西,这些东西更偏向于语法,需要大家动手去练习才能更好地记住。


目录
相关文章
|
1月前
|
程序员 C++
C++中的函数重载有什么作用
【10月更文挑战第19天】C++中的函数重载有什么作用
18 3
|
1月前
|
编译器 程序员 C++
C++中的函数重载是什么
【10月更文挑战第19天】C++中的函数重载是什么
25 0
|
1月前
|
自然语言处理 编译器 Linux
【C++】巧用缺省参数与函数重载:提升编程效率的秘密武器
【C++】巧用缺省参数与函数重载:提升编程效率的秘密武器
|
1天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
14 2
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4
|
15天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
43 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1