栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C、C++、C11学习笔记

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C、C++、C11学习笔记

  1. 编程基础
    1. 数据在内存中的存储
      1. 内存中的二进制存储时间,程序需要根据制定的数据类型去解析,这段数据本身也有读写的权限
    2. 虚拟内存
    3. ASCII编码
  2. C语言初探
    1. 源文件 .c .cpp,编译,生成二进制的目标文件(中间文件、临时文件),链接生成目标文件。那么在这个过程里面,也就是一个.cpp源文件对应一个中间文件。所以编程面向的最小对象就是.cpp文件,是一个有效作用域。
    2. 这里面,如果有头文件,涉及到重复引入头文件,那么头文件就要标记宏。多个中间文件的融合涉及到符合表。头文件里面一般只做声明,不做定义,如果做了定义,被多个中间文件使用,就会产生多个文件中有重复定义的变量,链接就会报错。头文件可以看成是全部复制到引入的.cpp文件开头。所以一个.cpp使用多个头文件要注意不能重复定义,多个.cpp使用同一个头文件,要注意符号冲突。
  3. 变量和数据类型
    1. 变量
      1. 变量本质是内存中的一段数据,然后声明告诉程序怎么解读目标区域的二进制数据。C是强类型语言,需要指定类型,后面也会有强制转换。
      2. 数据类型,其实到后面学习模板,类型本身也是一种数据,可以看成类型数据。
    2. 不同数据类型、长度、有符号、无符号、小数在内存中的存储
    3. 运算符,后面会学习重载运算符
    4. 数据类型转换
      1. 分为隐式和显式转换,C早前的时候强制类型转换,C++里涉及到转换构造函数和类型转换函数,深拷贝和浅拷贝,以及四种_cast关键字。
  4. 数据输入输出
    1. 这个就是scanf printf,有些比较繁琐的规则,但就主要就是看缓冲区的行为。C++里有peek函数可以瞄一眼缓冲区第一个字符,这个比较好用。
  5. 循环结构和判断结构
    1. for if else while 这些很常见的结构,在C11里面会比较灵活
          char arc[] = "afdadfd";
          //for循环遍历普通数组
          for (char ch : arc) {
              cout << ch;
          }
      
          char arc[] = "abcde";
          vectormyvector(arc, arc + 5);
          //for循环遍历并修改容器中各个字符的值
          for (auto &ch : myvector) {
              ch++;
          }
          //for循环遍历输出容器中各个字符
          for (auto ch : myvector) {
              cout << ch;
          }
          for (int num : {1, 2, 3, 4, 5}) {
              cout << num << " ";
          }
  6. C语言数组
    1. 数组可以看成是带了明确长度的指针,有点像array模板指定类型和长度,是静态数据。
  7. C语言函数
    1. 形参和实参
      1. 函数可能在这个cpp里面,调用在另一个cpp里面,所以有这样的一种形式。这里面涉及到对象的传递,可能是值传递、引用传递、指针传递
      2. return 对象时也会发生数据的拷贝,在内存里面
    2. 在栈上面调用函数,就是把返回地址压栈那一套,然后转到目标函数,执行。
    3. 函数声明,默认参数,全局变量,局部变量
    4. main() 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到 main() 函数,最后由 main() 函数结束整个程序。
  8. 预处理命令
    1. 「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。
    2. 使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:使用尖括号< >,编译器会到系统路径下查找头文件;而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
  9. 指针
    1. 指针变量
      1. 声明或者定义的时候要告诉编译器这是一个什么类型的变量,在变量名和类型名之间加一些符号,可以指出这是指针,左值引用,右值引用等信息。
        int a = 15;
        int *p = &a;
        int &p=a;
        int &&p=std::move(a);
    2. 数组指针。
      1. 与普通变量名相比,数组名既有一般性也有特殊性:一般性表现在数组名也用来指代特定的内存块,也有类型和长度;特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值。
    3. 指针函数,
      1. 返回值是一个指针的函数。
        int * func()
        {
            return ...;
        }
    4. 指针数组。
      1. 如果一个数组中的所有元素保存的都指针,那么我们就称它为指针数组。
        int *arr[3]={0x1234,0x2444,0x3344}
    5. 二维函数指针
      int (*p)[n];
    6. 函数指针
      #include 
      
      //返回两个数中较大的一个
      int max(int a, int b){
          return a>b ? a : b;
      }
      
      int main(){
          int x, y, maxval;
          //定义函数指针
          int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
          //如果写成 int *pmax(...,...);就成了函数声明
          //所以*和pmax要在一起,表明pmax是指针,不是函数名
      
          maxval = (*pmax)(x, y);
          return 0;
      }
    7. 复杂指针
      int *p1[6]; 
      int *(p2[6]); 
      //从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。从整体上讲,p1 是一个拥有 6 个 int * 元素的数组,也即指针数组。
      
      int (*p3)[6];  
      //从 p3 开始理解,( ) 的优先级最高,编译器先解析(*p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是一个拥有 6 个元素的一维数组。从整体上讲,p3 是一个指向拥有 6 个 int 元素数组的指针,也即二维数组指针。
      
      int (*p4)(int, int);
      //从 p4 开始理解,( ) 的优先级最高,编译器先解析(*p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针。
      
      char *(* c[10])(int **p);
      
      //c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。
      
      
      int (*(*(*pfunc)(int *))[5])(int *);
      
      //pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)。
  10. 结构体
    1. struct,创建变量的模板
    2. 枚举类型
      #include 
      //这些都是常量
      int main(){
          enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
          scanf("%d", &day);
          switch(day){
              case 1: puts("Monday"); break;
              case 2: puts("Tuesday"); break;
              case 3: puts("Wednesday"); break;
              case 4: puts("Thursday"); break;
              case 5: puts("Friday"); break;
              case 6: puts("Saturday"); break;
              case 7: puts("Sunday"); break;
              default: puts("Error!");
          }
          return 0;
      }
    3. Union
      1. 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
    4. 位域、位段
      1. 就是一个4字节内多放一些数据,并没什么用处。
      2. 如果穿插非位域成员,那么不压缩
        struct bs{
            unsigned m: 12;
            unsigned ch;
            unsigned p: 4;
        };
        //12字节
        
        struct bs{
            unsigned m: 6;
            unsigned n: 12;
            unsigned p: 4;
        };
        //4字节
        
        struct bs{
            unsigned m: 6;
            unsigned n: 25;
            unsigned p: 4;
        };
        //8字节
  11. 重要知识点补充
    1. typedef
      1. 就是很简单的类型别名,在C11里面会被using替代,一般都全大写
    2. const
      1. const在C里面有两个作用,一个是表明常量,一个是表明只读。在C11会有constexpr分担部分作用,然后使const着重表明只读。
      2. const 指针
        const int *p1;
        int const *p2;
        
        //表明p是一个指针,且指向的数据是只读的,就是说不能通过p1 p2去修改数据,p1 p2可以指向不同的区域
        
        
        int * const p3;
        //表明p3 是一个只读常量,不能改地址值,但指向的地址上的数据可以边。
      3. const 形参
        size_t strlen ( const char * str );
        //函数形参用const修饰,表明函数内部不会通过这个指针去修改这个形参指针指向的内存数据
      4. const 和非const 转换
        非const可以赋值给const,就是会降低了权限,不能通过const去修改。
        const表示当前变量只读,不代表他指向的内存不能被修改
        const int *a;
        int *b;
        如果指向同一个地址,只是不能通过a去修改,但仍旧可以通过b去修改。
        这里可以通过强制转换或者const_cast去进行类型转换。
  12. 文件操作
    1. C里面一般是FILE文件指针,C++里有fstream的,但总体上是类似的。
  13. C语言调试
    1. gdb需要另外学习
  14. C语言内存
    1. 虚拟内存和物理地址
      1. 虚拟地址
      2. 物理地址
      3. 页表映射
    2. 内存布局
      内存分区说明
      程序代码区
      (code)
      存放函数体的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。
      常量区
      (constant)
      存放一般的常量、字符串常量等。这块内存只有读取权限,没有写入权限,因此它们的值在程序运行期间不能改变。
      全局数据区
      (global data)
      存放全局变量、静态变量等。这块内存有读写权限,因此它们的值在程序运行期间可以任意改变。
      堆区
      (heap)
      一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。malloc()、calloc()、free() 等函数操作的就是这块内存,这也是本章要讲解的重点。

      注意:这里所说的堆区与数据结构中的堆不是一个概念,堆区的分配方式倒是类似于链表。
      动态链接库用于在程序运行期间加载和卸载动态链接库。
      栈区
      (stack)
      存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。
    3. malloc和free
      1. 对堆上空间分配的算法
      2. 内存池,池化技术
        1. 所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。
    4. 野指针
      1. 及时释放内存,并且置成NULL,在C11里有更多的内存回收机制。
    5. C的存储类别和生存期
      1.  存储类别就是变量在内存中的存放区域。在进程的地址空间中,常量区、全局数据区和栈区可以用来存放变量的值。
      2. C语言共有 4 个关键字用来指明变量的存储类别:auto(自动的)、static(静态的)、register(寄存器的)、extern(外部的)。
        1. auto 是缺省的一个,默认不写就是auto,可能是全局数据区,常量区,堆,栈上的变量。
        2. static 数据放在全局区(静态数据区),只能定义一次。生存周期直到程序结束,可以看成是一个全局变量。
        3. register 数据放在寄存器中,实际运行也不一定,要看资源。​
        4. extern说明在另外的cpp里定义,编译后需要从其他目标文件的符号表里找,从而引出多文件编程(多cpp、多目标文件)。
  15. 头文件编写
    1. 如果你一直在编写单个源文件的程序,那么请注意,全局变量的作用范围不是从变量定义处到该文件结束,在其他文件中也有效。
    2. extern用来声明。一般在头文件里,声明不限次数。
      1. 声明,就是告诉编译器我要使用这个变量或函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
      2. 函数声明
        1. 可以不加extern
      3. 变量声明
        1. 加了extern才是声明,不加就是定义,是直接有初始化的。
    3. 程序生成
      1. 文件类型说明实例
        可重定位文件
        (Relocatable File)
        这类文件包含了代码和数据,可以被用来链接成为可执行文件或动态链接库。静态链接库其实也是可重定位文件。Linux 下的 .o 和 .a,Windows 下的 .obj 和 .lib。
        可执行文件
        (Executable File)
        这类文件包含了可以直接执行的程序。Windows 下的 .exe,Linux 下的可执行文件没有固定的后缀,一般不写。
        共享目标文件
        (Shared Object File)
        这种文件包含了代码和数据,可以在以下两种情况下使用:一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件;第二种是动态连接器可以将几个共享目标文件与可执行文件结合,作为进程的一部分来运行。Linux 下的 .so,Windows 下的 .dll。
        核心转储文件
        (Core Dump File)
        当进程意外终止时,系统可以将该进程的地址空间的内容以及其他信息保存到核心转储文件。Linux 下的 core dump。
      2. 段  名说  明
        ELF Header文件头,描述了整个目标文件的属性,包括是否可执行、是动态链接还是静态链接、入口地址是什么、目标硬件、目标操作系统、段表偏移等信息。
        .text代码段,存放编译后的机器指令,也即各个函数的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。
        .data数据段,存放全局变量和静态变量。
        .rodata只读数据段,存放一般的常量、字符串常量等。
        .rel.text.
        rel.data
        重定位段,包含了目标文件中需要重定位的全局符号以及重定位入口,我们将在《符号——链接的粘合剂》一节中讲解。
        .comment注释信息段,存放的是编译器的版本信息,比如“GCC:(GUN) 4.2.0”。
        .debug调试信息。
        .line调试时的行号表,即源代码行号与编译后指令的对应表。
        Section Table段表,描述了 ELF 文件包含的所有段的信息,比如段的名字、段的长度、在文件中的偏移、读写权限以及其他属性。可以说,ELF 文件的段结构是由段表来决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的。
        .strtab字符串表,保存了 ELF 文件用到的字符串,比如变量名、函数名、段名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难,常见的做法就是把字符串集中起来存放到一个表中,然后使用字符串在表中的偏移来引用字符串。
        .symtab符号表,保存了全局变量名、局部变量名、函数名等在字符串表中的偏移。
      3. 段的合并
    4. 符号
      1. 全局符号,也就是函数和全局变量,它们可以被其他目标文件引用。
      2. 外部符号(External Symbol),也就是在当前文件中使用到、却没有在当前文件中定义的全局符号。
      3. 局部符号,也就是局部变量。它们只在函数内部可见,对链接过程没有作用,所以链接器往往也忽略它们。
      4. 段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如.text、.data等。
    5. 符号决议
      1. 在C语言中,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)
        1. 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
        2. 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
        3. 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
        4. 所以这个规则下,extern修饰的声明是弱符号,可以有重复的弱符号,但是只能有一个真正的定义,作为强符号。
    6. 头文件的书写,要防止被同一个cpp重复导入
      #ifndef _XYZ_H
      #define _XYZ_H
      
      #endif
    7. static变量和函数
      1. 如果正常定义,默认是所有cpp都能访问,为了避免强符号冲突,又不想去改变量名,可以使用static,变量只会在当前文件生效。
      2. 我们通常将不需要被其他模块调用的全局变量或函数用 static 关键字来修饰,static 能够将全局变量和函数的作用域限制在当前文件中。
      3. static 除了可以修饰全局变量,还可以修饰局部变量,被 static 修饰的变量统称为静态变量(Static Variable)。
        不管是全局变量还是局部变量,只要被 static 修饰,都会存储在全局数据区(全局变量本来就存储在全局数据区,即使不加 static)。
        全局数据区的数据在程序启动时就被初始化,一直到程序运行结束才会被操作系统回收内存;对于函数中的静态局部变量,即使函数调用结束,内存也不会销毁。
        注意:全局数据区的变量只能被初始化(定义)一次,以后只能改变它的值,不能再被初始化,即使有这样的语句,也无效。
  16. C++
    1. const
      1. C++中全局const变量可见范围是当前文件,这点有点像static。可以使用extern进行扩大。所以头文件里可以出现const的变量定义,因为每个cpp文件互不干扰。
    2. new delete 自动调用构造函数和析构函数。
    3. 内联函数
      1. 一般即时定义,是一个编译时的替换,并不会出现运行时栈调用的情况。
      2. 类定义时直接写在内部的都是内联函数。
      3. 内联函数是可以出现在头文件里的,因为编译时就替换了,所以不同cpp的目标文件里不会出现内联函数的代码,所以不会强符号冲突。
      4. 内联函数主要有两个作用,一是消除函数调用时的开销,二是取代带参数的宏。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。
    4. 默认参数
      1. C++规定,默认参数只能放在形参列表的最后。
      2. C++ 规定,在给定的作用域中只能指定一次默认参数。一般声明不指定,定义的时候指定。
        void func(int a, int b, int c = 36);
        void func(int a, int b = 5, int c);
      3. 声明可以多次,但在给定的作用域中一个形参只能被赋予一次默认参数。
    5. 函数重载
      1. C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。借助重载,一个函数名可以有多种用途。
      2. 参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。
      3. 参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值也不能作为重载的依据。
      4. 就是只有括号里面不一样就是重载。
      5. C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_int,void Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。
      6. 从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
    6. C混编 extern "C"
      #ifdef __cplusplus
      extern "C" {
      #endif
      
      void display();
      
      #ifdef __cplusplus
      }
      #endif
  17. 类和对象
    1. 类体的成员函数自动成为内联函数。
    2. C++对象的内存模型

    3. 构造函数
      1. 没有默认构造函数,就看自定义的构造函数,如果有自定义的构造函数,不会再生成默认构造函数。
      2. 构造函数初始化列表,搭配默认参数,可以兼容多个构造函数。
      3. const成员必须用初始化列表,相当于在对象生成时即时生成。
      4. 析构函数
      5. this指针
        1. this指向当前对象,是成员函数的一个隐式形参。
        2. this关联成员函数和成员变量,是成员函数和成员变量关联的桥梁。
    4. 静态成员变量和函数
      1. static成员变量,就是不管在类外定义,还是作为类的成员变量出现,都是存在全局变量区。
      2. static需要在类外初始化
      3. 所以本质上static的生存定义和普通的static是一样的,只是会被类的权限函数修饰,限制了访问的范围
      4. staitic成员函数只能使用static成员。
      5. static成员函数定义不加static
    5. const对象
      1. 常成员函数不修改成员变量值,所以能加就加,保护成员变量。
      2. const成员函数的声明和定义都需要const。
      3. 对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员了(包括 const 成员变量和 const 成员函数),因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做
    6. 友元函数和友元类
      1. 主要用于运算符重载
    7. 成员对象和封闭类
      1. 这个主要是说封闭类初始化时,先执行成员对象的构造函数,再完成自己的构造函数,这个很好理解,析构肯定也是先析构封闭类,再调用成员对象的析构。
  18. 引用
    1. 引用定义时初始化,需要告诉引用变量链接的是哪一个。&表示这个是一个左值引用变量,&&表示这是一个右值引用。
    2. 本质上就是定义后不能改变指向的指针,但是很多地方如果用指针就是 *p 的形式,引用可以干净一点。
    3. C11之前谈论引用都是左值引用,所以这个时候不能绑定到临时数据。
    4. 编译器会为const引用创建临时变量
      1. 其实从C11的角度而言就是 常左值引用,可以接受左值或者右值。普通左值引用不能接收右值。
      2. const引用形参可以接受非const和const的参数。
  19. 继承与派生
    1. public private protect
    2. 名字遮蔽问题
      1. 如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。
      2. 就是说在没有虚函数的时候,如果子类有同名的函数,函数参数都不同,那么会直屏蔽继承于父类的同名函数。两者不重载。子类对象调用所有父类的同名函数都会失效。
    3. 对象内存模型
    4. C++的多继承
    5. 虚继承和虚基类
      1. 虚继承

        1. 在内存模型里面,虚继承是放在后面的。
    6. 向上转型
      1. 根据对象的内存模型,可知派生对象可以给基类对象赋值,但反过来不行。
      2. 编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。
  20. C++多态与虚函数
    1. 虚函数
      1. 如果不用virtual修饰,如果子类有同名函数,那么子类对象就看不到父类的,父类指针指向子类对象的时候,去访问函数的时候,是访问父类类型的指针指向的函数,这不是我们想要的。
      2. 所以如果用virtual修饰后,相当于访问的是指向的对象自己实际的函数。
      3. 相当于用了虚函数,在调用函数的时候可能是父类,也可能是子类,多态。
      4. 引用也可以有多态,但是因为引用不好变换指向,所以一般都是指针
      5. 虚函数声明后,子类里面所有同名的(和遮蔽一样定义的同名)就会成为虚函数。
      6. 构造函数不能是虚函数,因为虚函数表示对象所有,调用构造函数时,对象还没创建完成。
      7. 析构函数可以不是虚函数,但在绝大多数有派生的场景下,父类的析构函数都要是虚函数,不然可能就会调用错误的析构函数。
    2. 纯虚函数和抽象类
      1. 就是只要有纯虚函数,一直都是抽象类,直到派生出一个实现所有纯虚函数的类。
    3. 虚函数表
      1. 就是对象生成的时候,基类的函数先进虚函数表,然后每次子类的虚函数出现遮蔽的时候会替代父类指定的虚函数,然后子类自己的新函数会加上去。如果不是虚函数,然后发生遮蔽,那么取决于指针类型。
    4.  RTTI机制
      1. 运行时才会确定对象的类型信息
      2. 如果有虚函数,对象内存还会增加type_info对象,记录类型信息。
  21. 运算符重载
    1. 运算符重载主要有两种,成员函数或者友元函数
      1. 成员函数比如运算符是单目的就可以用,并且不会有问题。
      2. 友元函数主要用于双目的运算符,因为涉及到转换构造函数。
      3. //复数类
        class Complex{
        public:
            Complex(): m_real(0.0), m_imag(0.0){ }
            Complex(double real, double imag): m_real(real), m_imag(imag){ }
            Complex(double real): m_real(real), m_imag(0.0){ }  //转换构造函数
            
            //这里三个构造函数在之后需要使用默认参数合并这三个
        
        public:
            //双目使用友元
            friend Complex operator+(const Complex &c1, const Complex &c2);
        public:
            double real() const{ return m_real; }
            double imag() const{ return m_imag; }
        
            //单目使用内部函数
            Complex operator++();  //++i,前置形式
        private:
            double m_real;  //实部
            double m_imag;  //虚部
        };
        
        //重载+运算符
        Complex operator+(const Complex &c1, const Complex &c2){
            Complex c;
            c.m_real = c1.m_real + c2.m_real;
            c.m_imag = c1.m_imag + c2.m_imag;
            return c;
        }
        
        Complex Complex::operator++(){
            ++m_real;
            return *this;
        }
        
        int main(){
            Complex c1(25, 35);
            Complex c2 = c1 + 15.6;
            Complex c3 = 28.23 + c1;
            ++c3;
            cout< 
  22. 模板
    1. 函数模
      1. #include 
        using namespace std;
        
        template void Swap(T &a, T &b);  //模板①:交换基本类型的值
        template void Swap(T a[], T b[], int len);  //模板②:交换两个数组
        
        void printArray(int arr[], int len);  //打印数组元素
        
        int main(){
            //交换基本类型的值
            int m = 10, n = 99;
            Swap(m, n);  //匹配模板①
            cout< void Swap(T &a, T &b){
            T temp = a;
            a = b;
            b = temp;
        }
        
        template void Swap(T a[], T b[], int len){
            T temp;
            for(int i=0; i 
    2. 类模板
      1. #include 
        using namespace std;
        
        template  //这里不能有分号
        class Point{
        public:
            Point(T1 x, T2 y): m_x(x), m_y(y){ }
        public:
            T1 getX() const;  //获取x坐标
        
        private:
            T1 m_x;  //x坐标
            T2 m_y;  //y坐标
        };
        
        template  //模板头
        T1 Point::getX() const  {
            return m_x;
        }
        
        int main(){
            Point p1(10, 20);
            cout<<"x="<Student stu2(stu1);  //以拷贝的方式初始化
        
        Student stu2 = stu1;  //以拷贝的方式初始化
        
        void func(Student s){}   //实参传给形参
        
        //返回值
        Student func(){
            Student s("小明", 16, 90.5);
            return s;
        }
        
    3. 深拷贝和浅拷贝
      1. 如果是默认拷贝构造函数,然后类里面又有自己的指针变量时,指拷贝指针,不自己new空间,就会出现逻辑上的错误。所以深拷贝就是要指针额外new自己的空间,显式的写拷贝构造函数。
      2. 所以重载=的时候要注意如果成员变量有指针,是不是另外new空间,而不是共用同一指针。
      3. 转换构造函数和类型转换函数,
        1. 两者逻辑上很像,但是只能出现一个,一般就是转换构造函数,容易出现二义性。
          //复数类
          class Complex{
          public:  //构造函数
              Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }  //包含了转换构造函数
          public:  //运算符重载
              //以全局函数的形式重载
              friend ostream & operator<<(ostream &out, Complex &c);
              friend istream & operator>>(istream &in, Complex &c);
              friend Complex operator+(const Complex &c1, const Complex &c2);
              friend bool operator==(const Complex &c1, const Complex &c2);
              friend bool operator!=(const Complex &c1, const Complex &c2);
              //以成员函数的形式重载
              Complex & operator+=(const Complex &c);
              //类型转换函数,这个会和转换二义性
              operator double() const { return m_real; }  
          public:  //成员函数
              double real() const{ return m_real; }
              double imag() const{ return m_imag; }
          private:
              double m_real;  //实部
              double m_imag;  //虚部
          };
          

      4. 借助默认参数,简化构造函数,合并普通的构造函数和转换构造函数。
        #include 
        using namespace std;
        
        //复数类
        class Complex{
        public:
            Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
        public:
            friend ostream & operator<<(ostream &out, Complex &c);  //友元函数
        private:
            double m_real;  //实部
            double m_imag;  //虚部
        };
        
        //重载>>运算符
        ostream & operator<<(ostream &out, Complex &c){
            out << c.m_real <<" + "<< c.m_imag <<"i";;
            return out;
        }
        
        int main(){
            Complex a(10.0, 20.0);  //向构造函数传递 2 个实参,不使用默认参数
            Complex b(89.5);  //向构造函数传递 1 个实参,使用 1 个默认参数
            Complex c;  //不向构造函数传递实参,使用全部默认参数
            a = 25.5;  //调用转换构造函数(向构造函数传递 1 个实参,使用 1 个默认参数)
        
            return 0;
        }
    4. 类型转换
      1. 隐式类型转换
        1. 就是有已有规则进行转换,比如
          1. double转int
          2. short转int
          3. 子类对象转父类类型
          4. 类里面自己有类型转换函数
        2. 隐式转换都是安全的
      2. 显式类型转换是直接按内存二进制去解读数据
    5. 类型转换运算符
      1. 关键字说明
        static_cast用于良性转换,一般不会导致意外发生,风险很低。
        const_cast用于 const 与非 const、volatile 与非 volatile 之间的转换。
        reinterpret_cast高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。
        dynamic_cast借助 RTTI,用于类型安全的向下转型(Downcasting)。
      2. static_cast
        1. 对应原来的隐式转换,安全地转换,不是暴力二进制直接转,会有内部转的规则或者函数。从写法上看,其实是增添麻烦,不如原先直接括号来得快。
          class Complex{
          public:
              Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
          public:
              operator double() const { return m_real; }  //类型转换函数
          private:
              double m_real;
              double m_imag;
          };
          
          int main(){
              //下面是正确的用法
              int m = 100;
              Complex c(12.5, 23.8);
              long n = static_cast(m);  //宽转换,没有信息丢失
              char ch = static_cast(m);  //窄转换,可能会丢失信息
              int *p1 = static_cast( malloc(10 * sizeof(int)) );  //将void指针转换为具体类型指针
              void *p2 = static_cast(p1);  //将具体类型指针,转换为void指针
              double real= static_cast(c);  //调用类型转换函数
             
              //下面的用法是错误的
              float *p3 = static_cast(p1);  //不能在两个具体类型的指针之间进行转换
              p3 = static_cast(0x2DF9);  //不能将整数转换为指针类型
          
              return 0;
          }
      3. const_cast
        1. const_cast 比较好理解,它用来去掉表达式的 const 修饰或 volatile 修饰。换句话说,const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。
          //所以说const 变量是只读,但不代表不可以更改。
          int main()
          {
              const int n = 100;
              int *p = const_cast(&n);
              *p = 234;
              cout<<"n = "<class Base{
          public:
              Base(int a = 0): m_a(a){ }
              int get_a() const{ return m_a; }
              virtual void func() const { }
          protected:
              int m_a;
          };
          
          class Derived: public Base{
          public:
              Derived(int a = 0, int b = 0): Base(a), m_b(b){ }
              int get_b() const { return m_b; }
          private:
              int m_b;
          };
          
          int main(){
          
              Derived *pd1 = new Derived(35, 78);
              Base *pb1 = dynamic_cast(pd1);
              cout<<"pd1 = "<#include 
          using namespace std;
          
          class A{
          public:
              virtual void func() const { cout<<"Class A"<(pa);  //向下转型失败
              if(pb == NULL){
                  cout<<"Downcasting failed: A* to B*"< func();
              }
              pc = dynamic_cast(pa);  //向下转型失败
              if(pc == NULL){
                  cout<<"Downcasting failed: A* to C*"< func();
              }
             
              cout<<"-------------------------"<(pa);  //向下转型成功
              if(pb == NULL){
                  cout<<"Downcasting failed: A* to B*"< func();
              }
              pc = dynamic_cast(pa);  //向下转型成功
              if(pc == NULL){
                  cout<<"Downcasting failed: A* to C*"< func();
              }
             
              return 0;
          }
  • 输入输出流和文件操作
    1. 这个比较刻板,相比较于C,C++的操作倾向于使用标准的输入输出和文件输入输出。
    2. peek()
      #include 
      #include 
      #include 
      using namespace std;
      string Months[12] = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug", "Sep","Oct","Nov","Dec" };
      int main()
      {
          int c;
          while((c = cin.peek()) != EOF) { //取输入流中的第一个字符进行查看
               int year,month,day;
               if(c >= 'A' && c <= 'Z') { //美国日期格式
                  string sMonth;
                  cin >> sMonth >> day >> year;
                  for(int i = 0;i < 12; ++i)  //查找月份
                         if(sMonth == Months[i]) {
                          month = i + 1;
                          break;
                      }
              }
              else { //中国日期格式
                  cin >> year ;
                  cin.ignore() >> month ; //用ignore跳过 "2011.12.3"中的'.'
                  cin.ignore() >> day;
              }
              cin.ignore();   //跳过行末 'n'
              cout<< setfill('0') << setw(2) << month ;//设置填充字符'',输出宽度2
              cout << "-" << setw(2) << day << "-" << setw(4) << year << endl;
          }
          return 0;
      }
    3. fstream去做文件的操作,替换原来的FILE
      #include 
      #include 
      using namespace std;
      int main() {
          const char *url ="http://c.biancheng.net/cplus/";
          //创建一个 fstream 类对象
          fstream fs;
          //将 test.txt 文件和 fs 文件流关联
          fs.open("test.txt", ios::out);
          //向test.txt文件中写入 url 字符串
          fs.write(url, 30);
          fs.close();
          return 0;
      }
  • C++多文件编程
    1. 其实和C的原理是一样的,就是一个cpp文件生成一个目标文件,然后链接。
    2. 当前cpp的全局变量是可以被其他cpp看到的
    3. const在头文件中,因为每个cpp里都一样,所以可以定义写在头文件里,正常的变量定义是不允许的,只能声明。
    4. 头文件里能写什么
      1. 声明
      2. const static修饰的对象,因为这些在cpp里展开后,会表明变量只在当前cpp里生效,所以不会被其他cpp的相同变量冲突
      3. 内联函数,即时定义,因为这个直接展开的,目标文件里不存在,所以不会被其他cpp冲突。
      4. 定义类。
  • auto
    1. C11的auto类型推导
    2. auto 必须初始化,(第一次声明时就定义)
      1. auto不能定义数组
      2. auto不用于模板参数 template 未定义加未定义
      3. 不能用于类定义中的非静态成员变量(因为还没有定义,静态的直接初始化成0)
    3. auto一般就用于迭代器和泛型编程,就可以省略不少参数,自动推导。总之有等号的地方可以用自动推导的都可以用auto。
      #include 
      using namespace std;
      
      class A{
      public:
          static int get(void){
              return 100;
          }
      };
      
      class B{
      public:
          static const char* get(void){
              return "http://c.biancheng.net/cplus/";
          }
      };
      
      template 
      void func(void){
          auto val = T::get();
          cout << val << endl;
      }
      
      int main(void){
          func();
          func();
      
          return 0;
      }
  • delctype
    1. 有些没有初始化的,auto用不了的地方可以想办法用decltype()来确定类型
      template 
      class Base {
      public:
          void func(T& container) {
              m_it = container.begin();
          }
      
      private:
          decltype(T().begin()) m_it;  //注意这里
      };
    2. 可以想办法找到合适的expr从上文的代码里,然后用delctype确定。
    3. decltype(左值或者(a)),返回的类型是一个引用。
    4. auto在推导指针或者引用时,保留cv限定符,否则会扔掉cv限定符。
      1. //非指针非引用类型
        const int n1 = 0;
        
        auto n2 = 10;
        n2 = 99;  //赋值不报错
        
        decltype(n1) n3 = 20;
        n3 = 5;  //赋值报错
        
        //指针类型
        const int *p1 = &n1;
        
        auto p2 = p1;
        *p2 = 66;  //赋值报错
        
        decltype(p1) p3 = p1;
        *p3 = 19;  //赋值报错

    5. auto推导会抛弃引用,decltype会保留引用
      1. #include 
        using namespace std;
        
        int main() {
            int n = 10;
            int &r1 = n;
        
            //auto推导
            auto r2 = r1;   //int 类型
            r2 = 20;
            cout << n << ", " << r1 << ", " << r2 << endl;
        
            //decltype推导
            decltype(r1) r3 = n;    //int & 类型
            r3 = 99;
            cout << n << ", " << r1 << ", " << r3 << endl;
        
            return 0;
        }
    6. 总之decltype更加的规则明确,但是auto简洁。
  • 返回值类型后置
    1. 就是函数的返回值通过delctype推导出返回值,然后搭配auto进行后置,这个肯定是用在模板里面的,因为还没实例类型化才可以推导,实例类型化了就不能后置。
    2. template 
      auto add(T t, U u) -> decltype(t + u)
      {
          return t + u;
      }
      
      
      int& foo(int& i);
      float foo(float& f);
      template 
      auto func(T& val) -> decltype(foo(val))
      {
          return foo(val);
      }
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 的三分大赛
  • 是
  • 转载请注明:文章转载自 www.mshxw.com
    本文地址:https://www.mshxw.com/it/960132.html
    我们一直用心在做
    关于我们 文章归档 网站地图 联系我们

    版权所有 (c)2021-2022 MSHXW.COM

    ICP备案号:晋ICP备2021003244-6号