• 【Cpp】 第一章 C++入门


    什么是C++

      C语言是面向过程式的语言,在处理小规模的问题时则能体现出其简单易上手的的优势,但是在面对大型程序或需要高度抽象化的程序时,C语言就显得略有鸡肋。在20实际80年代,计算机界为了解决软件危机提出了面向对象(OOP)思想的变成模式,于是支持OOP的编程语言也应运而生。

            C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计。

            C++拥有计算机运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。

            

            本章将从C++的语法上手入门。

    命名空间

            C语言中函数名可能会跟变量名冲突,如rand函数,再声明一个int型整数,名字为rand,就会起冲突。

     

    现在用C++的命名空间将rand其在里面

    1. namespace hello
    2. {
    3. int rand;
    4. }
    5. int main()
    6. {
    7. return 0;
    8. }

    结果,能成功生成

      此时,打印的rand是全局域的rand,想要访问命名空间域下的rand,需要在其前面加 "::"

    1. #define _CRT_SECURE_NO_WARNINGS
    2. #include
    3. #include
    4. namespace hello
    5. {
    6. int rand;
    7. }
    8. int main()
    9. {
    10. //全局域
    11. printf("%p", rand);
    12. //hello域
    13. printf("%p",hello::rand);
    14. return 0;
    15. }

    一个域内只能定义一个变量

    上面的rand出现问题,实际上是因为全局域中已有rand这一名字,而又在全局域声明一个rand就会冲突。(包含了有rand函数的stdlib.h头文件)

    ::  域作用限定符

    下面的代码,根据就近原则,将访问局部变量a

    1. int a = 0;
    2. int main()
    3. {
    4. int a = 1;
    5. printf("%d", a);
    6. return 0;
    7. }

    若想访问全局域变量,在前面加::

    如果:: 前没有域,则表示全局域

    1. int a = 0;
    2. int main()
    3. {
    4. int a = 1;
    5. printf("%d\n", a);
    6. printf("%d", ::a);
    7. return 0;
    8. }
    1. 1
    2. 0

    命名空间可以嵌套

           多个文件声明同名的命名空间,包含他们的时候,编译器会把这些命名空间内容合并到一起。

    使用命名空间

    1. 使用作用域限定符 ::
       
    2. 使用using namespace 将命名空间引入
      using namespace std;
    3. 使用using将命名空间中成员引入
      using std::cout

    缺省参数

    缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该 形参的缺省值,否则使用指定的实参。

    1. 为了不产生歧义,缺省参数只能从最后一个参数往前设置,并且中间跳过某个参数,要保持默认参数连续。
       
      1. void func(int a=10,int b;int c);
      2. void func(int a=10,int b;int c=10);
      3. 以上为错误缺省
      4. 正确缺省:
      5. void func(int a=10,int b=20;int c=30);
      6. void func(int a,int b=20;int c=30);
      7. void func(int a,int b;int c=30);
    2. 默认值只能是常量或是全局变量。
    3. 缺省参数不能在函数的定义和声明中同时出现。

      需要缺省参数时,声明在头文件较好。

    函数重载

    函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

    有以下几种不同构成重载

    1、参数类型不同
    2、参数个数不同
    3、参数类型顺序不同

    1. // 1、参数类型不同
    2. int Add(int left, int right);
    3. double Add(double left, double right);
    4. // 2、参数个数不同
    5. void f();
    6. void f(int a);
    7. // 3、参数类型顺序不同
    8. void f(int a, char b);
    9. void f(char b, int a);

    注意: 只有返回类型不同不构成重载,如:

    1. int func();
    2. void func();

    C++的重载

    下面示例两个函数,main调用func函数。

    func.c        main.c

    预编译 ↓

    func.i         main.i

    编译  ↓

    func.s        main.s

    汇编  ↓

    func.o         main.o

    链接  ↓

    xxx.exe

    预编译后等价于下图

    右图: main函数里执行func函数,实际上是调用了汇编代码的call指令,call再调用jump,jump找到func的函数指针,从而执行func函数。

    call的时候需要函数指针,这个函数指针在函数定义上,而函数定义在func.o。也就是说,如果只有main.o,是只能利用头文件的函数声明通过编译,但不能执行。必须在链接了func.o 和main.o后才能执行func函数。

    链接的作用:在符号表找对应的链接文件的地址,合并到一起。此时call指令就应该找到对应的地址了。

    C语言无法重载的原因:func在符号表里生成了函数地址,但是链接的时候,发现有2个func,无法区别,不知要调用哪个,所以无法重载。

     C++为了能实现重载功能,在编译阶段加入了一个修饰名的机制,使得在符号表里不会出现同名的情况,就能让链接不会出现不知调用哪个的情况。

    下面是linux下符号表生成函数名字的例子

    简单分析其中的命名规则,可以发现后缀的 id ii ,其实分别对应两个函数的参数(int,double) (int,int),这样就能使其实现重载的功能。

    【面试题】

    1. 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?

    1. int main() 
    2. {
    3. int ret = Add(10, 20); 
    4. cout << ret << endl;
    5. ret = Sub(30, 20); 
    6. cout << ret << endl; 
    7. return 0;
    8. }


    2. C语言中为什么不能支持函数重载?
    3. C++中函数重载底层是怎么处理的?
    4. C++中能否将一个函数按照C的风格来编译?

    C++使用C的库

    让c的程序生成一个静态库文件;C++编译环境去调用这个库,需要配置链接路径。

    同时还有C++命名与C命名规则不兼容的问题。如果此时从C++去调用函数,会在符号表里找不到对应的函数指针。

    所以需要用extern "C"表示按C的编译器编译。

    1. C++ 编译:
    2. extern "C"
    3. {
    4. #include "./XXX/sample.h"
    5. }
    6. 这里调用了非当前路径的头文件,因为找不到定义(sample.c)所以会有链接错误,故需要配置链接静态库路径。(VS百度搜索教程)
    7. 而因为是从C++下包含C,故需要把编译方式改为C:extern "C"

     C使用C++库

    因为C不兼容C++,所以只能将C++的库改成按C方式编译。

    引用

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

    1. 1. 引用在定义时必须初始化
    2. int a;
    3. int& b;

    1. 2. 一个变量可以有多个引用
    2. int a=0;
    3. int& b = a;
    4. int& c = a;
    1. 3. 引用一旦引用一个实体,再不能引用其他实体
    2. int a = 0;
    3. int& b = a;
    4. int a1 = 10;
    5. b = a1;

     

    此时未执行 b=a1 ;

    执行后

    可以看到b的地址并没有变成a1,而是跟着a一起变成了a1的值。

    说明引用初始化后,无法再引用其他实体,只能进行赋值操作。

    使用场景

     1.做参数

    1. void Swap(int& left, int& right)
    2. {
    3. int temp = left;
    4. left = right;
    5. right = temp;
    6. }

    2.作返回值

    先看看传值返回

    1. int Count()
    2. {
    3. int n = 0;
    4. n++;
    5. // ...
    6. return n;
    7. }
    8. int main(){
    9. int cnt=Count();
    10. }

     此方法并非将n直接返回给Count()函数,而是做了一份n的拷贝值,再传给函数返回。

    为什么会做一份拷贝?这是因为,如果函数内返回的为函数内部的局部变量,那么返回时会因为局部变量的销毁而返回失败。

     正确返回

    传址返回(a.输出返回值,b.大对象传参提高效率)

    1. int& Count()
    2. {
    3. static int n = 0;
    4. n++;
    5. return n;
    6. }
    7. int main(){
    8. int& cnt=Count();
    9. }

     只有用了静态变量的情况下,变量不会在返回的时候销毁,这个时候可以用传址返回。(如果不用传址返回,不管是否为静态变量都默认复制一份拷贝,再传值返回)

    错误例:

    1. int& Count()
    2. {
    3. int n = 0;
    4. n++;
    5. // ...
    6. return n;
    7. }
    8. int main() {
    9. int& cnt = Count();
    10. cout << cnt << endl;
    11. cout << cnt << endl;
    12. }
    1. 1
    2. 2058590600

    因为第一次调用时,该地址的值未被销毁,第二次再调用,函数及其内部局部变量已被销毁,所以返回了随机值。使用传址返回时要注意。

    注意:

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

    常引用

        int a 可以理解为可读可写
        const int a   则为 只读
        引用只能权限平移/权限缩小而不能放大,所以如果原变量只读,
        那么引用就不能放大成 可读可写。如下

    1. void TestConstRef()
    2. {
    3. const int a = 10;
    4. X int& ra = a; // 该语句编译时会出错,a为常量 (放大)只读->可读可写
    5. const int& ra = a;//(平移)只读->只读
    6. int a1=10;
    7. const int& ra1=a1; //(缩小)可读可写->只读
    8. // int& b = 10; // 该语句编译时会出错,b为常量
    9. const int& b = 10;
    10. }

    类型转换引用

    先看一般类型转换,int转到double时,中间会产生临时变量,然后再把临时变量赋值给d。

    类型转换,不论隐式转化还是强转,都会在中间产生临时变量,而不会操作原变量

    1. void TestConstRef() {
    2. double d = 12.34;
    3. × int& rd = d; // 该语句编译时会出错,类型不同
    4. const int& rd = d;
    5. }

    同样的,double到int时也会产生临时变量,但这个变量具有常性,故不能直接赋值给int&类型,需要再前面加const修饰。(可以理解为因为放大了权限,就出现编译错误)

      ​​

    引用和指针的不同点:

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

    如何回答?

    理解记忆:1.使用场景 ;2.语法特性及底层原理

    引用与指针的用途是相似的 

    1.返回输出,参数

    2.引用必须初始化,指针不需要

    指针更强大更危险,引用相对局限更安全

    语法角度:引用没有开空间,指针开了4/8 byte

    底层角度:引用是用指针实现的(反应在汇编代码上)

    内联函数

    众所周知,宏定义有优点有缺点:

    优点:1.复用性强;2.提高效率,减少栈帧建立。

    缺点:1.可读性差;2.没有类型安全检查;3.不方便调试

    内敛函数功能类似于宏定义,在符合条件的地方展开,而不需要开辟栈帧。

    内敛函数几乎将宏的缺点都优化掉,相对于宏定义的优点是:

    1. 可以区分类型;
    2. 可读性更强
    1. //宏定义
    2. #define ADD(a,b) ((a)+(b))
    3. //内联函数
    4. inline Add(int a,int b)
    5. {
    6. return a+b;
    7. }

    c++中尽量用enum,const,inline 替代宏

    inline在vs下,若展开代码过多,也会选择不进行优化,即不进行展开编译。

    func{ 100行代码 }

    main { 调用1w次 func函数 } 

    展开:     [call] 1w*100      (每次都展开,每次都要100空间)

    不展开 :[call] 1w次+100(调用1w次,但只用100空间)

    展开可能导致可执行程序变大

     内敛函数不要将声明与定义分离char。

    auto关键字(C++11)

    语法

    auto能自动识别右值的类型,赋给左值,并让左变量变成该类型。

    1. auto a = 'a';
    2. cout<<typeid(a).name<

    输出

    char

     另外auto类型声明时必须初始化。

     使用细则

    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。

    看如下的代码

    1. int num=10;
    2. auto x=#
    3. auto* y=#
    4. auto&z =num;

    x得到num的地址所以类型是int*;y得到的也是num的地址,故也是int*,其auto* 表示强调是指针类型,后面必须传指针类型给y

    z前为auto& ,表示强调这是引用。

    1. int num=10;
    2. auto x=# //int*
    3. auto* y=#//int* 强调为指针
    4. auto&z =num; //int 强调为引用

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

    语法

    for( 类型 变量名 : 数组名){  };

    取代了以往书写较为繁琐的for循环。

    同时内部单独每个变量类型能用auto自动识别来替代

    1. for (auto a : arr)
    2. {
    3. cout << a;
    4. }

     结合auto的强调引用类型auto&,就可以实现改变数组的值(如果不用引用类型, 得到的只是拷贝值)

    1. for (auto& a : arr)
    2. {
    3. a++;
    4. }

    指针空值nullptr(C++11)

            C++中用nullptr来表示空指针,而不使用NULL。C++中的NULL实际是一个宏,在传统的C头文件(stddef.h)中,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量

    注意:

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

    本节入门到此结束0.0

  • 相关阅读:
    b站小土堆pytorch学习记录—— P18-P22 神经网络+小实战
    王者荣耀改名神器助手微信小程序
    python基于django的商品比价平台
    kafka部署SASL_PLAINTEXT协议设置身份认证
    6.2 List和Set接口
    iOS 判断触摸位置是否在图片的透明区域
    思腾云计算
    HFS局域网分享文件的神器(附下载链接)
    vue集成海康h5player实现播放
    antd table表格支持多选框选择当前列,进行格式设置等
  • 原文地址:https://blog.csdn.net/qq_44041312/article/details/125954009