欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > 【C++】初识C++之C语言加入光荣的进化(下)

【C++】初识C++之C语言加入光荣的进化(下)

2024/12/28 10:34:32 来源:https://blog.csdn.net/m0_71580879/article/details/144708404  浏览:    关键词:【C++】初识C++之C语言加入光荣的进化(下)

写在前面

书接上文:初识C++(上)。本篇笔记作为C++的开篇笔记,主要是讲解C++关键字(C++98)连带一点点(C++11)的知识。掌握的C++新语法新特性,当然C++是兼容C的,我们学习C的那套在C++中也是受用。


文章目录

  • 写在前面
  • 六、引用
    • 6.1、引用权限的细节
    • 6.2、引用的使用场景
    • 6.3、引用和指针的区别
  • 七、内联函数
  • 八、auto关键字(C++11)
    • 8.1、auto的使用细节与注意事项
        • 1. 一般`auto`与指针和引用结合起来使用
        • 2. 在同一行定义多个变量
        • 3. `auto`不能作为函数的参数
        • 4. `auto`不能直接用来声明数组
        • 5. 为了避免与C++98中的`auto`发生混淆,`C++11`只保留了`auto`作为类型指示符的用法
        • 6. auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有跟以后会讲到的lambda表达式等进行配合使用。
  • 九、 基于范围的for循环(C++11)
  • 十、指针空值nullptr(C++11)


六、引用

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

引用的定义类型& 引用变量名(对象名) = 引用实体;
在这里插入图片描述
给一个普通变量设置一个引用:

int main()
{int a = 10;int& ra = a;//<====定义引用类型printf("%p\n", &a);printf("%p\n", &ra);return 0;
}

程序运行结果:
在这里插入图片描述

  • 在上面程序中,我们就成功把变量ra设置为了a变量的别名
  • 程序运行结果中,也可以看出两个变量的地址都是同一空间

注意引用类型必须引用实体同种类型

引用的使用细节:

  1. 引用一旦引用一个实体,再不能引用其他实体
  2. 引用在定义时必须初始化
  3. 一个变量可以有多个引用

在这里插入图片描述
例子1:不进行初始化赋值。

int main()
{int a = 10;int& b;int& ra = a;int& num = a;printf("%p\n", &num);printf("%p\n", &ra);return 0;
}

程序运行结果:
在这里插入图片描述
例子2:更改引用变量,查看引用变量是否会改变为引用其他实体。

int main()
{int a = 10;int b = 30;int& ra = a;ra = b;//尝试更改引用变量raprintf("%p\n", &ra);printf("%p\n", &a);printf("%p\n", &b);printf("\n%d\n", a);printf("%d\n", ra);return 0;
}

程序运行结果
在这里插入图片描述

  • 在上运行结果图中,可以看出 ra = b执行的是赋值b变量的值,赋值给raraa的别名,实际上就是更改a变量的值。 说明:引用一旦引用一个实体,再不能引用其他实体
  • 由这个例子说明了,别名只能在初始化的时候进行赋值过了初始化后,其他情况下=都是进行赋值操作,不再进行初始化操作。

6.1、引用权限的细节

引用变量也是变量,也支持变量中的权限设置,如:把变量设置常变量的const、设为静态变量的static。在引用中,有着严格的权限管理,引用的权限可以缩小和平移,但绝对不能放大

在这里插入图片描述

例子1: 引用的缩小权限

int main() {double d = 12.34;const double& rd = d;rd = 2.5;//errd = 2.5;return 0;
}

程序运行结果:
在这里插入图片描述

  • 在上图中我们把d的设置了引用rd,但是 rd是被const收索了权限,把 rd 设置为了一个常变量,这时我们使用rd进行更改变量值是,就会报错,但是 rd收缩权限并不会影响变量d

我们注释rd变量后打印结果,如下如:
在这里插入图片描述

例子2:权限的平移

int main() {const int a = 10;const int& ra = a;cout << a << " and " << ra << endl;return 0;
}

程序运行结果:
在这里插入图片描述

例子3:尝试权限放大

int main() {const int a = 10;int& ra = a;ra = 20;cout << a << " and " << ra << endl;return 0;
}

程序运行结果:
在这里插入图片描述

通过上面的三个例子,就很好理解引用权限细节。


6.2、引用的使用场景

1.做函数的形参 (一般用于输出型参数,减少拷贝提高效率)

在C语言时期,我们要写一个交换函数,函数的形参就必须是指针,在函数中的编码也是相当的麻烦:如下程序

void Swap(int* a, int* b) {  int num = *a;*a = *b;*b = num;
}

在程序中,我们只要涉及到形参变量交换或赋值,就需要解引用,这样我们编码复杂度提高了,这时候我们使用引用,为传递过来的实参取别名,这样我们不需要解引用就可以直接操作到实参。如下程序:

void swap(int& ra, int& rb) {int num = ra;ra = rb;rb = num;
}int main() {int a = 10, b = 20;swap(a, b);cout << "a = " << a << " b = " << b << endl;return 0;
}

程序运行结果:
在这里插入图片描述

这样我们编写程序时候,就无需每次使用形参都进行解引用操作,大大增加了编码的效率

2.做函数的返回值(提高效率)

在不使用引用作为返回值的时候,我们的返回值一般都是使用传值返回。如下程序

int add(int a, int b) {int sum = a + b;return sum;
}int main() {int a = 10, b = 20;int sum = add(a, b);cout << "a + b = " << sum << endl;return 0;
}
  • add函数的返回值会传递给寄存器。之后,再把寄存器的值赋值给变量sum。(如下图)在这里插入图片描述

在需要有返回的函数中一定进行生成寄存器,在返回值比较小的时候才使用寄存器,如果但返回值大的时候,就使用临时变量。在传值返回时,免不了把需要返回的内容拷贝给寄存器/临时变量,这样效率会有损耗,如果说返回类型是个巨无霸,那么所消耗的时间也是非常恐怖。所以我们可以使用引用返回来提高效率

int& add(int a, int b) {//引用返回static int sum = a + b;return sum;
}int main() {int a = 10, b = 20;int num = add(a, b);cout << "a + b = " << num << endl;return 0;
}

在上面函数中,我们设置了引用返回,那么改函数返回的就是sum的别名(引用)。此时就没有再进行中间的赋值。这样就节省下了时间
在这里插入图片描述
注意: 使用引用返回返回的变量一定不能局部变量如果返回的是局部变量,那么该变量会随着函数的生命周期结束而结束。那么得到的引用值是随机值(类似于野指针)。(如下图)
在这里插入图片描述
在这里插入图片描述

所以如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。

3.做函数的返回值(修改返回值+获取返回值)

在C++中把struct自定义类型升级为类,在类中可以声明变量,也可以定义函数。在C的结构体中如果我们定义了一个数组,那数组的使用会非常繁琐,如下程序

struct test {int arr[500];};int main() {struct test t1;t1.arr[0] = 0;t1.arr[0]++;return 0;
}

每次使用结构体中的数组,都需要结构体变量名.结构体变量的编写,才可以调用到对应的数组,但是在C++ 中,使用函数封装后,再使用传引用返回,如下程序:

struct stu {
public:int _name[1000];int& at(int pos) {if (pos >= 0 && pos < 1000) {return _name[pos];}}
};int main() {stu test1;int& arr1 = test1.at(10);arr1 = 0;	return 0;	
}

使用传引用返回后,我们也用引用来接收,这样arr1就是_name[pos]的别名,这时我们就可以用arr1轻松修改返回值,从而改变对应的 “结构体变量”

  • int& arr1 = test1.at(10);:此时test1.at(10)返回的是_name[10]的别名,我们用引用变量arr1来接收,这么arr1也是_name[10],所以可以直接使用arr1来修改。

C++的写法是:C++类里面可以进行运算符重载(不才后面笔记会补充这个内容),在重载后不需要再和C语言和引用接收一样需要复杂的编写了。如下程序:

struct stu {
public:int _name[1000];int& operator[] (int pos) { // 重载下标引用操作符[]if (pos >= 0 && pos < 1000) {return _name[pos];}}};int main() {stu test1;test1[2] = 0;test1[2]++;}

重载下标引用操作符[] 后我们就可以直接使用使用类变量+操作符就可以直接访问到,对应的 “结构体变量”,因为在重载下标引用操作符函数中,我们使用的是传引用返回,返回的数组对应下标的别名,即 []实际是_name[pos]


6.3、引用和指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。但是在实际底层实现上实际是有空间的,因为引用类似指针方式来实现的。如下图汇编代码
在这里插入图片描述
在上图的汇编代码中。我们清晰地看到。引用和指针的汇编代码都是一样的。说明引用起始也是由指针实现的,由编译器封装实现了引用的结构。但是不能说引用就是指针

在这里插入图片描述

int main() {double a = 0;double& ra = a;double* pa = &a;cout << sizeof(ra) << "   " << sizeof(pa) << endl;return 0;
}

在32位环境下运行的结果:在这里插入图片描述

  • 我们知道在32位环境下,指针占用空间大小是四个字节。double类型占用八个字节。
  • 在程序运行结果中,我们也可以看到。我们计算引用的大小其实是计算的是变量类型占用空间的大小
  • 计算指针变量的大小,计算的是指针类型的占用空间的大小

引用和指针的不同点:

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

七、内联函数

inline修饰的函数叫做内联函数编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

在C语言中,如果我们频繁的调用函数时,在内存中就会不断的栈帧与销毁,这样会导致效率下滑。如下代码:

int add(int a, int b) {return a + b;
}int main() {int a = 10, b = 20;for (int i = 0; i < 100000; i++) {arr(a, b);cout << i << endl;}return 0;
}
  • 在循环中,我们循环的栈帧与销毁十万次只为了将两个变量相加,效率极其低下。
  • 我们只能更改为使用宏函数,这样避免十万次栈帧与销毁的开销。
#define ADD(x,y) ((x) + (y))//int add(int a, int b) {
//	return a + b;
//}int main() {int a = 10, b = 20;for (int i = 0; i < 100000; i++) {ADD(a, b);cout << i << endl;}return 0;
}
  • 使用宏函数虽然可以避免了十万次的栈帧与销毁开销,但是使用宏函数还有明显的缺点的

关于宏函数:

  1. 优点: 不需要建立栈帧,提高效率
  2. 缺点:复杂,容易出错,可读性差,不能调试

所以在C++中就推出了内联函数,只需要在普通函数前,增加一个inline关键字即可。

inline int add(int a, int b) {return a + b;
}int main() {int a = 10, b = 20;for (int i = 0; i < 100000; i++) {cout << add(a, b) << endl;}return 0;
}
  • 这样就把add函数更改为内联函数

我们查看汇编代码中可以清晰看出:编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

非内联函数:
在这里插入图片描述
内联函数:
在这里插入图片描述

但是inline对于编译器而已也只是建议最终决定该函数是否在调用内联函数的地方展开的决定权在编译器,就算我们把每个函数都设置为内联函数,也不确保把每个函数都时内联函数。

因为在函数内容过多的情况下,每个调用内联函数的地方都展开的话,会造成代码膨胀。
在这里插入图片描述

一个函数func,里面由100行代码,在整个工程中,由10000处调用了该函数。

此时若func函数它不是一个内联函数时,整个工程在编译后的func占用代码量只是:10000+50行代码。

若此时func函数时一个内联函数,而且每次都会在调用的地方展开,那么整个工程在编译后func占用的代码量就是:10000*50行代码。
在这里插入图片描述
最终结果就导致可执行程序变大,使得最终程序安装包也变大。

我们实际测试一下:

inline int add(int a, int b) {cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;cout << "Bucai_不才" << endl;return a + b;
}int main() {int a = 10, b = 20;for (int i = 0; i < 100000; i++) {cout << add(a, b) << endl;}return 0;
}

程序运行结果:
在这里插入图片描述

总结:内联函数只用于短小而且频繁调用的函数,对于行数较长的函数与递归函数不能使用内联,当然编译器也会自己决定。
ps:在vs默认Debug环境下,inline不会起作用,因为这样就不方便调试了。

inline不建议声明和定义分离,分离会导致链接错误。因为inline编译器默认展开的,不会进入符号表,就没有函数地址了,链接就会找不到。

在这里插入图片描述

//test.h
inline int add(int a, int b);//add.c
int add(int a, int b) {return a + b;
}//main.c
int main() {int a = 10, b = 20;add(a, b);return 0;
}

运行结果:

在这里插入图片描述

  • 上图报的是链接错误。我们画图详细分析。

main.c中:
在这里插入图片描述

  • 在编译阶段,发现add函数只有声明,那么就创建一个符号表记录add函数,待链接时候再把定义的地址链接在一起。在这里插入图片描述

  • .h文件中,add函数声明的是inline内联函数,在编译阶段编译器不会把add函数生成符号表,因为编译器认为该函数在预编译时候就已经展开了。

  • 到最后链接阶段却发现,main函数的符号表中的add函数,没有对应的定义函数的地址,所以报错。

若我们修改成一下代码,内联函数就会成功运行:

//test.h文件
inline int add(int a, int b);
int testadd(int a, int b);//add.c
int add(int a, int b) {return a + b;
}int testadd(int a, int b) {int sum = add(a, b);return sum;
}
//main.c
int main() {int a = 10, b = 20;cout << testadd(a, b) << endl;return 0;
}
  • 我们在add.c文件内调用add函数,在预编译期间,把头文件内容拷贝到add.c文件中,可以得到下内存图在这里插入图片描述
  • 此时在add.c文件内,内联函数add有声明和定义,所以在testadd函数中可以直接展开。

总结:不才认为内联函数是不能声明和分开定义的,上面的例子中我们可以感受出,内联函数声明和定义分开并没有意义,在同一声明和定义在文件下可以起到内联作用,但是没必要分开,在不同文件下无法成功调用函数的定义


八、auto关键字(C++11)

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写
  2. 含义不明确导致容易出错
#include <string>
#include <map>
int main()
{std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };std::map<std::string, std::string>::iterator it = m.begin();while (it != m.end()){//....}return 0;
}
  • 在上代码中std::map<std::string, std::string>::iterator是一个类型,但是该类型太长了,特别容易写错。

我们可以使用typedef重命名类型,如下:

#include <string>
#include <map>
typedef  std::map<std::string, std::string>::iterator Map;
int main()
{std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };Map it = m.begin();while (it != m.end()){//....}return 0;
}

但是使用typedef也会有有一个问题,就是在以const修饰指针缩小权限的时候,会出现我们意料之外的结果。如下代码

typedef char* pstring;
int main()
{const pstring p1;	// 编译成功还是失败?const pstring* p2;	// 编译成功还是失败?return 0;
}

程序运行结果:

在这里插入图片描述

  • const pstring p1:编译器会const修饰到变量p1缩小指针变量的权限,使得p1变为一个常变量,即使pstringchar*
  • const pstring* p2:在p2上,const修饰的就是*p2,缩小的是*p2的权限,并不会把p2变量改变为一个常变量。

在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

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替换为变量实际的类型

这样,我们再次使用定义std::map<std::string, std::string>::iterator类型时,就不需要再手敲,就可以直接使用auto来充当类型。如下:

int main()
{std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };auto it = m.begin();while (it != m.end()){//....}return 0;
}

8.1、auto的使用细节与注意事项

1. 一般auto与指针和引用结合起来使用

auto声明指针类型时,用autoauto*没有任何区别,但用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;
}
2. 在同一行定义多个变量

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

在这里插入图片描述

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

在使用auto作为函数的形参时候,编译器无法在编译期间对形参的实际类型进行推导。

void test(auto a) {printf("%d\n", a);
}
int main() {test(10);auto a = 10;return 0;
}

程序运行结果:
在这里插入图片描述

我们尝试加上缺省值:
在这里插入图片描述

  • 即使我们使用了缺省参数的形式来使用auto也是报错。
4. auto不能直接用来声明数组

在这里插入图片描述

5. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
6. auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有跟以后会讲到的lambda表达式等进行配合使用。

九、 基于范围的for循环(C++11)

在C++98和C语言中如果要遍历一个数组,可以按照以下方式进行:

int main() {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;return 0;
}

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

在这里插入图片描述

int main()
{int arr1[] = { 1, 2, 3, 4, 5 };char arr2[] = { 'a','b','c','d' };for (auto& e : arr1) {e *= 2;}for (auto e : arr1) {cout << e << " " ;}cout << endl;for (auto e : arr2) {cout << e << " ";}return 0;
}

程序运行结果:

在这里插入图片描述

  • 左边变量我们可以设置为auto类型,这样可以用来接收所有类型的值。
  • 右边则是有范围的集合,会由编译器逐个赋值给左边接收变量
  • 上面范围for解析(以arr1为例):范围for依次取arr1数组中数据赋值给e,自动迭代结束。
  • 需要注意的是,如果我们想修改数组里面的内容时,我们就需要把接收变量设置为引用

注意: 与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

范围for的注意事项:

  1. for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供beginend的方法beginend就是for循环迭代的范围。
在这里插入图片描述
举个错误例子:

void TestFor(int array[])
{for (auto& e : array)cout << e << endl;
}

运行结果:在这里插入图片描述

  • 在函数中,我们知道array变量已经不是函数,而是指针,指针没有范围所有不能作为范围for的迭代的范围。
  1. 迭代的对象要实现++和==的操作。

十、指针空值nullptr(C++11)

在C语言中,NULL在库中是被宏重命名的数字0,如下图:
在这里插入图片描述
可以看到,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;
}

程序运行结果:在这里插入图片描述

在运行结果中,我们可以观察到,我们使用NULL调用的重载函数居然是int类型的函数,程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将NULL看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0

如下图:(在64位环境下,指针变量占用8个字节)
在这里插入图片描述

  • NULL计算的是int类型占用4个字节的大小
  • 只有强制类型转换(void*)NULL时,才是指针变量的大小。

为了避免这样的情况出现,在C++11中就新增了 nullptr作为空指针,使用nullptr代表的就只是指针,不再回出现看作整形常量的情况。


以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有用的话,就请多多为我点赞收藏吧~~~💖💖
请添加图片描述

ps:表情包来自网络,侵删🌹

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com