当前位置: 首页 > news >正文

C++入门(笔记)

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!

       

目录

1.C++的发展

2.命名空间

2.1.命名空间引入

2.2.命名空间的相关符号和方式

2.3 命名空间的使用

3.输入和输出

3.1 输出

3.2 输入

3.3 cout 和cin 的特点

4.缺省参数

4.1全部缺省

4.2半缺省

4.3 缺省参数的声明和定义

5.函数重载

5.1使用形式

5.2 重载的原理

6.引用

6.1 引用的格式

6.2 引用的特性

6.3 常引用

6.4 使用

6.5 引用和指针的对比

7.内联函数

8.auto关键字

9.指针空值问题


本章节是C++语言的入门,将介绍C++是如何弥补C语言不足的。

1.C++的发展

         什么是C++

        C语言适合处理较小规模的程序。不适合处理复杂的问题,规模较大的程序,

        为了解决软件危机, 20世纪80年代,计算机界提出了面向对象思想。 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++

        总体来说C++源于C语言,在C++的文件中仍然兼容C语言,C++有C发展过来,更加的成熟,适用处理规模大且复杂的问题。

2.命名空间

2.1.命名空间引入

        从C语言引出问题,看下面的程序以及注释,assert是C语言库中的一个函数,断言,用来告诉程序员程序出错的函数,但是对他命名之后程序还是可以运行,输出的是局部的变量。

#include <stdio.h>
//创建全局变量assert 其作用域为整个文件
int assert = 10;
int main()
{//创建局部变量,起作用域为它所在的花括号中int assert = 20;//输出的变量:遵循就近原则,在有局部变量的情况下,局部变量优先,//在没有局部变量的情况下,才是全局变量。printf("%d\n",assert);return 0;
}

        但是在加上下面这行代码的时候再运行,就有问题了。

#include <assert.h>

        注意:加上上面的这行代码,整个程序也是可以运行的,兄弟们。assert在<assert.h>中是通过#define来定义的,宏的定义,不是真正的函数,宏在预处理阶段会被替换成对应的代码;因此在处理变量assert的时候不会和宏定义的assert冲突,因为assert在预处理阶段就已经被处理了转换成其他代码了,而变量的处理会在编译和链接部分。

        所以上述代码中的assert替换为malloc,整体代码如下:

#include <stdio.h>
#include <stdlib.h>
//创建全局变量assert 其作用域为整个文件
int malloc = 10;
int main()
{//创建局部变量,起作用域为它所在的花括号中int malloc = 20;//输出的变量:遵循就近原则,在有局部变量的情况下,局部变量优先,//在没有局部变量的情况下,才是全局变量。printf("%d\n",malloc);return 0;
}

        这个时候它肯定会报错:

        分析错误,当我不包含<stdlib.h>文件的时候, 程序可以正常的输出mallloc 为20;但是在包含之后,就会报出重定义的错误。

        原因就是:包含<stdlib.h>文件之后,相当于将头文件中的内容拷贝到此文件中,在<stdlib.h>中,将malloc定义为函数,但是在上述程序中,将其定义为整型变量,因此为重定义。

疑问:

  1. 当全局变量 int malloc = 10;删除的时候会不会报错。
  2. 当malloc函数在main函数中适用的时候会不会报错
#include <stdio.h>
#include <stdlib.h>int main()
{//创建局部变量,起作用域为它所在的花括号中int malloc = 20;//输出的变量:遵循就近原则,在有局部变量的情况下,局部变量优先,//在没有局部变量的情况下,才是全局变量。printf("%d\n",malloc);//在创建局部变量之后适用mallocint* a = (int*)malloc(sizeof(int));if (a == NULL){perror("malloc fail");}printf("%p\n", a);free(a);a=NULL;return 0;
}

答案:

        1.不会报错,对main函数中的malloc相当于为局部变量,头文件包含展开的相当于全局全局变量,全局变量和局部变量之间存在优先级的问题,在局部函数执行完毕后,局部变量将消失。

        2.malloc是在main函数中的变量,在创建具备怒变量之后,因此下面的malloc()会认为是malloc=20;语法不合规,变量的遮蔽,局部变量malloc覆盖了标准库中malloc()函数相当于,当然会报错,属于C语言中未定义错误。

int* a = (int*)(20)(sizeof(int)); 

总结:

        C语言中的变量命名在同一个命名域(相当于C语言中作用范围)下是不可以同名的,但是对于复杂庞大的项目并且是多人对接的情况下,避免不了命名相同,因此C++引入了命名空间。

2.2.命名空间的相关符号和方式

        在C/C++中,变量、函数和类的名称会存在域全局作用域中,为防止定义冲突,使用命名空间将标识符(名称)本地化(封存在一个独立的空间中),避免命名冲突记忆、名字污染。

        使用 namespace 定义的命名空间域(作用域也是域的一种),域分为:类域、命名空间域、全局域、局部域。

        在使用namespace之前先介绍一个符号 :: 为作用域解析运算符,使用方式 :作用域名称::变量/函数,当作用部分是空的时候访问的是全局域。

#include<iostream>
//定义全局变量
int num = 10;
int main()
{//定义变量int num = 20;printf("%d\n",num);//输出全局变量printf("%d",::num);}

下面正式进入namespace的学习

       (1).定义变量

在下面的代码中,我分别在全局、局部、命名空间域中定义了名字相同的整型变量,但是别名有报错,全局变量中的num和命名空间域中的num并没有冲突,因为有了namespace进行了隔离。

#include<iostream>
//定义全局变量
int num = 20;
//定义命名域
namespace my_project
{int num = 30;
}
int main()
{//定义变量int num = 10;//输出局部变量printf("%d\n",num);//输出全局变量printf("%d\n",::num);//输出命名域中的变量printf("%d", my_project::num);
}

总结

        访问num的顺序:局部域 ,全局域、命名空间域(不指定是不会去搜索的)不会主动的去命名空间域里面去搜索,

        什么情况下才可以到域中去搜索; 展开了命名空间域 /指定访问命名空间域。

        上述的三个num可以同时存在,只要在不同的域就可以同时存在;

        如何展开命名空间呢?

using namespace my_project;

        上述为展开命名空间的操作 using为关键字,也可以展开std标准库,但是一般不会这么使用,因为这样用和定义全局变量是一样的,将整个命名空间域展开,会造成其和全局变量冲突。

        再看下面这段代码

#include<iostream>
#include<stdlib.h>
namespace my_project
{int malloc = 20;
};int main()
{cout << malloc << endl;//输出的是malloc函数的地址cout << my_project::malloc << endl;//调用是my_project域中的malloc定义
}

        std是C++标准库(包含STL和其他部分),将标识符放在独立的作用域中,防止和用户的命名冲突,cout相当于输出函数,cin为输入函数,endl为换行符,为std中规定好的。

        上面的命名方式不建议这么写。建议个英文相关含义的单词来命名,例如蛇形命名法,中间加上一个下划线,max_valueuser_agetotal_count等等

        (2)命名空间中定义函数

        命名空间中也可以定义函数,和C++的标准库一样。

#include <iostream>
using namespace std;
namespace my_project
{int Add(int x, int y){return x + y;}
};
int main()
{cout << my_project::Add(2, 10) << endl;}

     (3)命名空间域的嵌套

          在一个命名空间域中再插入一个命名空间域,是有必要的,加入所创建的命名空间域的比较复杂, 而且其中的有重定义的部分,就可以在一个命名空间域中,再加一个命名空间域。

//乘法 Multplr 
#include <iostream>
namespace my_project
{int a = 10;int Add(int x, int y){return x + y;}namespace N2{int b = 30;int  Multplr(int x, int y){return x * y;}};
};
using namespace std;
int main()
{cout << my_project::Add(2, 10) << endl;cout << my_project::N2::Multplr(2, 10) << endl;
}

        可以使用::访问第一层命名空间域之后,再访问其内部的嵌套的命名空间域。

2.3 命名空间的使用

        命名空间的使用其实就是访问并调用命名空间中的变量和函数;namespace将加变量和函数进行了隔离,所以要使用一个关键词进行展开,使用using可以展开命名空间或者展开其中的摸一个变量或者一个函数,主要注释均在程序里面。

#include <iostream>
namespace my_project
{int a = 10;int Add(int x, int y){return x + y;}namespace N2{int b = 30;int  Multplr(int x, int y){return x * y;}};
};
using namespace std;//展开C++标准库的命名空间域
//第一种使用方式,展开全部的域
using namespace  my_project;//展开定义的空间域
using namespace  my_project::N2;//展开嵌套的空间域
//第二种使用方式,将命名空间域中的某个变量或者参数引入
using   my_project::Add;//展开定义的空间域
using   my_project::N2::Multplr;int main()
{//第三种使用方式,就是使用作用域解析运算符号直接使用了。cout << my_project::Add(2, 10) << endl;cout << my_project::N2::Multplr(2, 10) << endl;
}

     

第一种方式在日常的练习中可以使用,但是在项目中不会用的。全部展开的缺点

        下面可以加std也可以不加STD,展开和不是展开头文集按头文件不一样的,

        展开是指的是编译器编译的时候会去这个命名空间去搜索然后向上寻找,找的时候要有std的定义,没有被std的定义就找不到了; 库中的东西封在STD里面的。直接展开会有风险我们定义如果跟库重名了,就报错了;建议项目里面不要去展开,库文件中的没有重名则不会报错,库也定义了,一旦包含就会重复报错,日常练习可使用第一种。

第二种方式:指定展开

        将常用的符号展开可以,但是展开的多就和全展开一样了;项目建议指定访问,不可轻易展开命名空间

3.输入和输出

3.1 输出

        使用cout标准的输出对象,(控制台),其定义在<iostream>头文件中,std是C++标准库的命名空间域,作用就是将C++标准库中的符号和用户所写的符号进行隔离,因此想要使用cout就要访问std的命名空间。后期再理解一下。需要搭配<<操作符,掺入操作符。

//头文件定义和声明cout、cin
#include <iostream>
//将C++标准库中的符号和用户的符号隔离,避免冲突,使用的时候再展开
using namespace std;
//上面代码是将所有的命名空间域展开
int main()
{cout << "hello world!" << endl;return 0;
}
3.2 输入

        使用cin标准的输入对象,由键盘来输入,需要配合>>操作符,提取操作符。

#include <iostream>
using namespace std;
int main()
{cout << "Enter your age" << endl;int age;cin >> age;cout << "you are " << age << " years old"<<endl;
}
3.3 cout 和cin 的特点

        cout和cin的可以自动识别数据的类型,下面的代码由三种数据结构,但是只是使用cin和cout直接插入和输出就可以了。

#include <iostream>
using namespace std;
int main()
{int a;double b;char c;// 可以自动识别变量的类型cin >> a;cin >> b;cin >> c;cout << a << endl;cout << b << endl;cout << c << endl;return 0;
}

需要注意的是

        在输出双精度浮点型的时候会丢失精度,

       可以和 c语言混用,cout和c语言混用的情况下,C++需要兼容C语言的缓冲区;因此是要比printf和scanf更加费时间。

4.缺省参数

        缺省参数就是吗,默认参数,在定义或者声明的时候给形参赋予的一个值,当实参没有值的时候,函数将采用设定的默认参数来执行对应的操作

4.1全部缺省

        将形参为的默认值分别设为10和20,输出二者之和,

//函数缺省
#include <iostream> // 其中包含stdio和stdlib
using namespace std;//全缺省函数
void Add(int x = 10, int y = 20)
{cout << x + y << endl;
}
int main ()
{Add();Add(20);Add(20,30);return 0;
}

        栈帧调用的压栈;从左向右与依次传参;不可以跳过,全缺省,所有参数给了默认值  不可以指定传参。也就是说下面这行代码的调用时错误的。

	Add(,30);
4.2半缺省

        半缺省就是在声明和定义函数的时候从左向右给缺省值,中间不能跳过,缺省值必须要连在一块,类似于下面这样的,缺省值要从右边给起。因为函数传参是从最左边开始的,如果定义为void Func(int a=10, int b, int c=30);那么调用函数就会是这样的Func(,10),这种语法是挖法实现的。函数调用时,编译器按从左到右的顺序匹配实参和形参。

//半缺省参数 从右向左给的缺省值
void Multplr(int x , int y = 20,int z = 5)
{cout << x * y * z << endl;
}
int main ()
{Multplr(1);Multplr(1,2);Multplr(1,2,3);return 0;
}
4.3 缺省参数的声明和定义
  缺省参数不能在函数声明和定义中同时出现

        声明和定义在同一个文件中,且声明和定义出现在调用函数之前的情况,下方。

        C++ 编译器按顺序处理代码,每个 .cpp 文件是一个独立的编译单元。当编译器遇到函数调用时,它需要知道:函数的签名(参数类型和返回值)是否有默认参数

        如果函数定义(带默认参数)出现在调用点之前,编译器可以直接从定义中获取这些信息,因此声明可以省略(或者声明中可以不写默认参数)。

#include <iostream> // 其中包含stdio和stdlib
using namespace std;
//声明
void Multplr(int x , int y , int z );
//定义
void Multplr(int x = 10, int y = 20, int z = 5)
{cout << x * y * z << endl;
}int main ()
{Multplr(1);Multplr(1,2);Multplr(1,2,3);return 0;
}

        将定义放到main函数后面,就会报错,在 main 函数中调用该函数时使用默认参数会导致编译错误。这是因为编译器在处理函数调用时,只能看到声明(而声明中没有默认参数),无法获知定义中指定的默认参数。

综上所属,一般情况下,缺省函数一般放在声明中。

        在不同的文件下,我么你分别在Test.cpp文件中写出驻韩书用来测试,在Multplr.cpp文件中写出函数Multplr函数的定义,在Multplr。h文件中写出函数的声明,键给函数的缺省参数放在了头文件中。这样写的代码测试可以运行的。

//Test.cpp文件
#include"Func.h"
int main ()
{Multplr(1);Multplr(1,2);Multplr(1,2,3);return 0;
}
// Multplr.cpp文件
#include"Func.h"
//定义
void Multplr(int x , int y , int z )
{cout << x * y * z << endl;
}
//Multplr.h文件
#include <iostream>
using namespace std;
//声明
void Multplr(int x = 10, int y = 20, int z = 30);

         上述代码是可以运行,但是为什么函数的缺省参数要放在声明中,头文件中呢;        

        在C++工程文件中,每个 .cpp 文件是独立编译的。如果默认参数只在 .cpp 文件中定义,则其他包含该头文件但未直接包含该 .cpp 文件的代码无法得知默认参数值,导致编译错误。还有就是C++ 标准规定:如果函数在多个地方声明(如不同的头文件被不同 .cpp 文件包含),则所有声明中的默认参数必须相同。将默认参数放在头文件中可确保一致性。

5.函数重载

5.1使用形式

        在C++中,在同一个作用域中,可以声明几个功能类似的同名函数,同名函数的形参列表不同,形参列表包括参数个数、类型、类型顺序;函数重载可以处理实现功能类似数据类型不同的问题。

#include<iostream>
using namespace std;//参数的种类不同int Addi(int x, int y)
{cout << "int ++  " ;return x + y;
}
double Addd(double x, double y)
{cout << "double ++  ";return x + y;
}//参数的个数不同void Func()
{cout << "空" << endl;
}
void Func(char a )
{cout << a<< endl;
}//参数的类型顺序不同void Sub(int x, double y)
{cout << "int x, double y" << endl;}
void Sub(double x ,int y)
{cout << "double x ,int y" << endl;
}// 主函数int main()
{cout << Addi(2, 2) << endl;cout << Addd(2.2, 2.4) << endl;	char a = 'a';Func();Func(a);Sub(2, 3.3);Sub(3.3, 2);return 0;
}

上述代码分别从参数的种类 、参数种类的顺序、参数的数量展示了函数重载

5.2 重载的原理

(1)前置知识:

        函数名的修饰:编译器为了在编译后区分同名函数以及包含函数在内的符号而采用的一种机制。

举例:对于函数 void func(int a),修饰后的名字可以为_Z4funci; 其重载函数

void func(double b),修饰后的名字可以为_Z4funcd;

        因此,在C++去区分重载函数,并精确的调用重载函数,就是给重载函数替换一个函数名字。

下图是C/C++程序编译链接流程

重载函数在C++ 编译流程中实现的过程:        

        预处理阶段将头文件中的声明中展开;

        编译阶段,编译器会根据函数名+参数列表生成每个函数对应的符号表(内部名),因此,函数名相同,但是参数列表相同,经过编译器的修饰会生成唯一的内部名;所以编译器可以通过修饰后的唯一的名称,区分重载函数。

        汇编阶段:在目标文件中,函数将以“修饰后的名字”存在,为后续链接做准备。

        链接阶段:链接器会查找所有的目标文件里的符号,并进行关联;

        对于重载函数调用处的函数名会被替换为“修饰后的名字”,链接器会根据这个唯一名称,精准找到对应的函数实现。

        最后合并多个目标文件,生成可执行程序。

        (2)在VS编译器中调用函数的流程

        假设调用是函数StackPush(&st, 2);先经过一个反汇编,然后call指令,在跳转到Jump指令,再跳转到函数执行的地址。下图就是函数调用流程了

需要注意  假设  Stack.h文件中为声明,Stack.cpp文件是函数定义;Test.cpp文件为调用函数测试文件。

        链接之前生成的目标.o的文件,Test.o无法调用Stack.o中的文件;

        在链接阶段,链接器将文件合并,生成可以执行的文件就可以调用了。

       CALL指令的生成,其实在编译阶段就已经存在了,但是其指向的地址是未解析的符号引用,地址是未知的,仅仅作为符号存在。在来链接阶段链接器解析符号,将 CALL 指令地址修正为函数的实际地址。当然不是所有的函数都是需要链接时候调用的,将定义和调用放在同一个文件中,叫做本地定义,编译器直接生成相对地址的 CALL 指令。。

 C语言只支持函数名的调用,不支持函数名的修饰,因此无法支持重载。还不会Gcc,就不演示了啊。

总结:

(1)C语言不支持函数重载,因为他只支持函数名调用函数

(2)C++支持函数重载,在编译阶段,编译器会更具函数名和函数参数列表重新生成新的函数名(符号表),在链接时候调用是不会嗲用错误的。

        返回值问题,返回值无法决定函数是否能进行重载;因为在函数修饰命名规则不变的情况下,举个例子

void func();     // 返回void
int func();      // 返回intfunc();          // 调用哪个?无返回值时无法选择

        而没有参数的函数或者参数一致函数,调用的时候会存在歧义的,所以返回值不行,除非修改规则的。

6.引用

        引用是给已经存在的变量取得一个别名,暂时可以理解为变量和其引用共用一块内存空间。

        C++中的引用将取代C语言中的指针使用;比指针更加的好用。

6.1 引用的格式

数据类型& 引用名称 = 引用实体;引用的格式。

    int a = 10;int& b = a;
//b是a的引用,相当于的另一个名字,访问b就是访问a。

当然需要注意的是,变量和其引用必须是同一个类型的数据。

共用内存空间的说明

int a = 10;int& b = a;int& c = a;int& d = a;int& e = a;//输出地址cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;cout << &e << endl;

运行结果

6.2 引用的特性
  • 引用在定义时必须初始化;
  • 一个变量可以有多个引用;
  • 引用只能是一个变量的引用,且不可以修改
  • 别名的别名也是变量的别名。

       (1) 引用必须初始化问题,引用不是独立存在的对象,是已有变量的别名它与被引用的对象共享内存地址,因此必须在定义时绑定到一个对象;而且引用一旦绑定到某一个对象,无法绑定到其他对象;假如允许未初始化的引用,可能导致空引用,而且将无法检测到。

        (2)一个变量就可以有多个引用,但是一个引用只能配对一个变量

    int a = 10;int& b = a;int& c = a;int& d = a;int& e = a;//输出变量cout << a << endl;cout << b << endl;cout << c << endl;cout << d << endl;cout << e << endl;

函数参数的引用

void swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}void swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}void swap2(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;swap1(a,b);cout << a << " " << b << endl;//没有交换成功,这里再使用函数重载部分swap(&a, &b);cout << a << " " << b << endl;//交换成功,传递指针swap2(a, b);cout << a << " " << b << endl;//交换成功,形参设置为变量的引用,相当于自身的修改。return 0;
}

        在这里说明一下,虽然swap1和swap2构成了函数重载,但是调用的时候会存在歧义,但是一般这种情况不会存在。毕竟传值调用和传递实参的引用是不一样的。

       

    (3)引用只能是一个变量的引用,且不可以修改

        引用的底层通常用常量指针实现,一旦初始化就不能改变指向:

int a = 10;
int& ref = a;  // 等价于:int* const ptr = &a;

     (4) 变量引用的引用也是变量的引用。

    int a = 10;int& ra = a;int& rra = ra;cout << a <<" "<< ra << " " << rra << endl;

        (5)补充

        在Java语言中,变量的引用相当于变量的指针,是可以修改的;在链表创建的时候,可以变量的引用来创建结点;但是C++是不可以的,因为C++规定的是变量的引用是无法修改的。但删除中间结点的时候,变量的引用无法修改,链表就会断开。

6.3 常引用

      (1)  常引用就是无法通过修改引用来达到修改变量本身的目的,相当于常量的指针,不能通过解引用的来修改指针指向的变量。

(2)常引用中存在一个权限问题,在引用的过程中权限是不可以放大的,权限只能平移或者缩小。

分为四种情况

  • 权限缩小或者平移可以;
  • 权限放大不可以;
  • 常量的引用;
  • 常引用的类型转换;
  • 函数返回值的权限缩小和放大。
    //正常代码,只有权限的缩小int x = 0;int& y = x;const int& z = x;x++;//z++;属于z的权限放大,不可以使用cout << x << endl;

        上述代码的可以通过x和y修改变量的值,但是不能通过z来修改,其是不可以修改的值。

//权限放大const int a = 0;//int& b = a;//权限放大问题// 变量赋值const int c = 0;//c为常量是不可以修改的const int& d = c;//其应用也必须是常引用,d也是不可以修改的int e = c;//赋值是是没有问题的//常量的别名 const int& m = 10;//数据类型的转换double dd = 2.22;//int i = dd; const int& ri = dd;//dd会发生整型提升,产生临时变量,临时变量具有常性cout << ri << endl;

函数返回值的权限缩小和放大。

int func1()
{static int x = 0;return x;}int& func2()
{static int x = 0;return x;
}int main()
{//对于func1的返回值权限int ret1 = func1();// 值的拷贝 没有问题int& ret1 = func1();//权限放大,x返回的值为常量,不会const int& ret1 = func1();//权限平移//对于func2的返回值权限int ret1 = func2();// 值的拷贝 没有问题int& ret1 = func2();//权限平移, 没有问题const int& ret1 = func2();//权限缩小,没有问题}
6.4 使用

前置知识:

        输入型参数:函数从调用者获取数据,在函数内部使用但不修改实参。

        特点:参数通常为值传递;含糊内部不i改变调用者的数据。

        输出型参数:函数通过参数向调用者返回数据,修改实参的值。

        特点:参数通常为指针或者引用;函数内部会改变调用者的数据。

        总结:

                     输入型函数;形参的改变不会影响实参;

                     输出型函数:实参的改变不会影响形参。

(1)做形式参数

//C语言
void swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}
//C++
void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}

        函数中改变的值,实参也会跟着改变。

引入一个例子:

void LTPushBack(struct ListNode** pphead, int x)//链表的后插入 C语言
void LTPushBack(ListNode& phead, int x)//链表的后插入 C++

(2)做返回值

        C语言静态返回值

//	C语言静态变量做返回值
int Count()
{static int n = 10;//变量存放在静态区,n++;return n;
}int main()
{int ret = Count();cout << ret << endl;return 0;
}

静态变量的特点:

  1. 生命周期较长:静态区变量的生命周期和程序的运行周期是一样的。在程序运行期间,这些变量始终存在,不会因为定义它的函数执行结束就被销毁。

  2. 自动初始化:静态区变量如果没有进行显式初始化,会被自动初始化为 0(对于数值类型变量)或者空指针(对于指针变量)。这能减少因未初始化变量而产生的错误。

  3. 内存位置固定静态区变量的内存地址在编译时就已经确定,在程序运行期间不会发生改变。这一特性让变量可以在不同的函数调用之间保持其值,实现数据的共享。

  4. 降低内存分配开销:由于静态区内存是在程序启动时就分配好的,不需要像栈内存或者堆内存那样在运行时进行动态分配和释放,这样就减少了系统的开销。

  5. 简化数据共享:在多个函数需要访问同一个变量的情况下,使用静态区变量是一种简单的实现方式。不过要注意,过度使用全局静态变量可能会导致代码的可维护性变差。

        只要函数是传值返回,不管n是静态区的变量还是普通的变量,均会生成一个临时变量;用来承载返回值,并将返回值拷贝到主函数变量中,经理两次拷贝。

        在C语言中,当函数设置有返回值的时候,随着函数执行完毕,栈帧的消除,局部变量也会消除,返回值会先存储在一个临时变量中(可能是寄存器,但是其大小仅有4、8个字节,不足以承载更大的数据), 栈区的临时变量也是会消除的;栈帧是向下生长的。

        引用返回值,返回的是静态变量n的引用。

//引用做返回值
int& Count()
{static int n = 10;//变量存放在静态区,n++;return n;
}int main()
{int ret = Count();cout << ret << endl;return 0;
}

       返回值为引用的时候;不会创建中间变量来存储返回值,引用本质上是原变量的别名,直接指向原变量的内存地址;当函数返回的时候,相当于返回的是原变量的地址,ret会从该地址读取到的数据复制到ret中;n为静态区变量,其生命周期与运行周期相同;因此即使函数栈帧消失,n的内存仍然有效,返回其引用是安全的。可以将引用理解为变量指针的复制,但是赋值或者使用的时候相当于变量的本身。

int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);//c的引用的引用是ret,因此改变c也会导致ret的改变,//但是这个程序本身就是错误的,c在计算成功时候就已经消除了,无法使用引用来传值Add(3, 4);printf("sssssssssssss\n");cout << "Add(1, 2) is :" << ret << endl;return 0;
}
       什么情况下可以使用返回值为变量的引用;函数返回的时候,函数栈帧消除完成,如果返回对象还在(内存没有返回给操作系统),可以使用引用返回,如果已经还给系统了,则必须使用传值返回,因为你定义的引用的源头已经没有了。这中情况相当于访问野指针。 
        野指针形成的原因:指针没有初始化没有置空;对已经释放内存的指针,在没有置NULL的情况下为野指针;返回值的指针是已经销毁的变量的指针;内存访问越界情况,访问的是未定义过的指针,因此是野指针。

如何看引用是否为同一个变量的引用,看地址

int& Count()
{int n = 0;cout << &n << endl;n++;return n;
}int main()
{int& ret = Count();cout << &ret << endl;return 0;
}

        地址相同为同一个变量的引用或者变量自身。

另外:

      用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
6.5 引用和指针的对比

        以下面代码转反汇编语言

    int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;

        左边是引用的反汇编,右边是指针的反汇编,在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

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

其他的一些不同点

引用指针
概念本质变量的别名,操作引用即操作原变量存储变量地址的容器,通过地址找变量
初始化要求定义时必须绑定有效变量(否则编译报错)定义时可暂不指向变量(支持空指针)
绑定 / 指向可变性一旦绑定,终身不能换绑其他变量随时可修改指向,换存其他同类型地址
空值支持无空引用(必须关联有效数据)支持空指针(如 NULL/nullptr )
sizeof 结果等于所绑定变量的类型大小(如 int 占 4)等于地址存储字节数(32 位占 4,64 位占 8)
自增(++)行为原变量的值 +1(操作的是变量本身)指针地址 跳过一个类型大小(指向下一数据)
多级嵌套不支持多级引用(语法不允许)支持多级指针(如 int** )
访问实体方式编译器自动关联原变量,直接用引用名需显式解引用(* 或 -> )
安全性更安全(无需判空、不会换绑出错)需谨慎(空指针、野指针易引发崩溃)

7.内联函数

知识复习

        #define 使用宏定义符号,会在预处理阶段将其替换为相应的值或者表达式;宏函数不需要建立栈帧,可以提高调用的效率;缺点就是容易出错,可读性较差,而且能调试。可以单独定义字符常量,但是不建议其定义函数。

内联函数的定义

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

        内联函数的优点:不需要建立栈帧,不复杂,可读性不错、不容易出错、可以调试。

        适用的场景:适用于短小的频繁调用的函数,太长的函数会导致代码膨胀。

       举个例子加深理解,在debug模式下是不可以显示内联函数的,需要在release的情况下,查看汇编代码是否是否含有call指令。

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

上述代码只是函数的调用,其汇编代码中存在call指令来调用函数。

当我加上inline将Add函数改为内联函数的时候.

        当引入内联函数的时候,将不会存在压栈指令以及函数调用指令;其直将内联函数在调用的地方展开。

注意:

         当在调用内联函数的时候,编译器也会进行筛选,当内联函数的函数体太大(存在递归、循环)的情况下,函数是不被编译器认为i是内联函数的;而且内联函数也不能使用太多次,当内联函数被调用太多次的时候,代码体积膨胀原理:内联会将函数体复制到每个调用点。若函数被频繁调用(如循环内调用 10 万次),代码量可能呈指数级增长。

举个栗子

        当一个函数Func编译之后有50行的指令,在这段代码中,我调用了10000次这个函数;代码中和这个函数相关的指令函数有几行?

        不是内联函数的时候是10050行;是内联函数,代码就会膨胀到500000行、。

        因此内联函数适合多次调用,但是不适合大量多次的调用;并且函数体也要适当。

注意:

        内联函数的声明和定要在同一个文件中,因为对于内敛函数,在编译的过程中,就是在这个地方展开;

        内联函数相当于没有地址的函数,并且内联函数是不会进入符号表的,是不会被调用的。

再举个函数体过大的栗子;增加函数体的大小的时候。

inline int Add(int x, int y)
{int i = 100000;cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";cout << i << " ";return x + y;
}

编译器直接判定函数不再是内联函数

8.auto关键字

               auto的作用就是自动识别表达式右边的数据类型

    int a = 0;int b = a;auto c = b;auto d = 1 + 1.11;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;

        上述代码中的auto可以自动识别表达式右边数据的类型。

typeid(x).name();  // 获取x的类型信息

        上述代码是获取x的类型信息。后续C++的使用中,迭代器中的数据类型的名字较长,可以使用auto减少工作量。

注意的是

        

        使用auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型 。因此 auto 并非是一种 类型 的声明,而是一个类型声明时的 占位符 ,编译器在编 译期会将 auto 替换为变量实际的类型,(较为复杂,目前阶段未理解)
        遍历数组的方式
    int arr[] = { 0,1,2,3,4,5,6,7,8,9,10 };int i = 0;//以下标打印for (i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)cout << arr[i] << " ";cout << endl;//以地址打印for(int*ri=arr;ri<arr+ sizeof(arr) / sizeof(arr[0]);++ri)cout << *ri << " ";cout << endl;//语法糖for (auto e: arr){cout << e << " ";}cout << endl;

        上述语法糖的形式等价于遍历数组的第二种形式;但是更加的简洁和方便。也叫做范围for

for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。其普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

        范围for的使用条件: 

        for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供 begin和 end 的方法, begin end 就是 for循环迭代的范围。

9.指针空值问题

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

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

        因此建议使用nullptr代替NULL要进行强制转换的情况;nullptr就是专门表示 “指针空值”;

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

http://www.lqws.cn/news/562357.html

相关文章:

  • MySQL 索引 -- 磁盘,主键索引,唯一索引,普通索引,全文索引
  • AC自动机 多模式字符串匹配(简单版)
  • 马斯克的 Neuralink:当意念突破肉体的边界,未来已来
  • 嵌入式原理与应用篇---ARM
  • 深度学习量化数值类型
  • 机器学习——线性回归
  • 数据结构与算法学习笔记(Acwing 提高课)----动态规划·单调队列优化DP
  • Requests源码分析:底层逻辑
  • 模板方法 + 策略接口
  • glog使用详解和基本使用示例
  • 数据结构:顺序表
  • Lua现学现卖
  • Java代码阅读题
  • 06-three.js 创建自己的缓冲几何体
  • 某音Web端消息体ProtoBuf结构解析
  • 【网络安全】网络安全中的离散数学
  • 机器学习算法-K近邻算法-KNN
  • BUUCTF [ACTF新生赛2020]music 1
  • SpringMVC系列(五)(响应实验以及Restful架构风格(上))
  • 【学习】《算法图解》第七章学习笔记:树
  • [论文阅读] 软件工程 | 微前端在电商领域的实践:一项案例研究的深度解析
  • Linux软件的安装目录
  • 【面板数据】省级电商指数与地级市电子商务交易额数据集(1990-2022年)
  • OpenLayers 下载地图切片
  • Docker安装MinIO
  • 概述-4-通用语法及分类
  • 【go】初学者入门环境配置,GOPATH,GOROOT,GOCACHE,以及GoLand使用配置注意
  • 案例开发 - 日程管理系统 - 第一期
  • Redis 实现分布式锁
  • 【C++进阶】--- 继承