学习一年Java的程序员的C++学习记录(指针引用绕晕记)

文章目录

    • 一 C++入门
    • 二 变量和数据类型
    • 三 运算符
    • 四 流程控制
    • 五 复合数据类型
    • 六 函数
    • 七 函数高阶
    • 八 面向对象

一 C++入门

  1. 标准输出流中 cout 是一个ostream对象,<<>>是C++中经过重载的运算符,配合cout和cin使用时表示流运算符。C++中是如何重载运算符的?

  2. cin 从键盘读取输入的时候,首先会忽略掉开头的任意多个空格输入,只会从第一个非空格符、制表符和回车符开始读取。开始读取后,cin会以一个回车符作为输入的结束标志,但不会读取这个回车符!因此这个回车符还会保存在输入缓存中。

    学习一年Java的程序员的C++学习记录(指针引用绕晕记)_第1张图片

  3. 运算符优先级:比较运算符 大于 赋值运算符

  4. 为什么需要.h头文件?因为头文件中包含了一些全局属性的声明。在C++中,函数可以直接定义在类之外,因此有函数和方法的概念。函数是哪些直接定义在类外的,而方法是定义在类内部的。函数和方法的结构和功能都一致,仅仅是位置不同。定义在类之外的函数,是全局可用的,这点和类一致。因此,需要在使用某个函数的时候进行声明。当函数的声明变多的时候,头文件就起到了包含这些所需要声明方法的作用。一般的,某个源文件中通过同名的头文件来进行声明,需要使用该源文件中功能的时候,包含其头文件即可。

二 变量和数据类型

  1. C++是有一门强类型的语言(与Java相似,而Python是一门弱类型的语言)
  2. 作用域:每个{}内部定义的变量只在此{}内部有效,出了{}后变量无效,这样的变量被称为全局变量。如果是定义在所有{}之外的变量,则被称为全局变量,是全局可访问的。这点和函数、类一致。
  3. static关键字:在不同的地方的作用有所不同
    • 如果是在全局范围(所有{}之外的范围就是全局范围)或者命名空间范围内,static关键字指定变量或函数为内部链接,即该变量或函数仅仅在文件内部是可见的,外部无法进行访问。在声明变量时,变量具有静态持续时间,即生命周期从程序启动时新生到到程序结束时死亡。并且除非您指定另一个值,否则编译器会将变量初始化为0。
    • 如果是在函数范围内,static关键字指定变量只初始化一次,并在调用该函数后保留其状态,但是变量的作用域还是在该函数范围内。
    • 如果是在类内部声明成员变量,static关键字指定该类所有实例共享该变量的副本。必须在文件范围内定义静态数据成员。(与Java一致)
    • 如果是在类内部声明成员方法。static关键字表示可以通过类名对该方法进行调用。因此静态成员方法无法调用非静态成员变量,因为此时没有生成对象。(与Java一致)
  4. C++中,整型默认是可正可负的。对于每种类型的整型,C++都提供了一个无符号的类型。无符号数据类型在底层是如何存储的呢?应该还需要额外的字段来存储数据类型吧。
  5. char类型也是一种整数类型,可以直接定义char为0~127之间的整数,在显示时,计算机会将char类型显示为字符。如果char类型参与数值计算,显示时将作为整型显示。
  6. bool类型也是一种整数类型。bool类型有两种取值,true和false。可以直接定义bool类型变量为整型。但bool类型只会保存0和1。当定义bool为0时,bool会保存0;当定义bool为任何非0的值时(包括负数),bool都将保存1。
    • python、C++都可以讲bool类型和int类型隐式类型转换,在if表达式中可以直接使用1 和 0来表示true和false。
    • Java无法将boolean和int类型进行转化
  7. 在赋值时,字面值常量有自己的数据类型,变量被定义为某种数据类型,编译器会将字面值常亮转化成变量的数据类型,从而赋值给变量。这也解释了为什么所有非0的整数赋值给bool的时候都会转化成1。
  8. C++在赋值时的隐式类型转换非常广泛,可以是:
    • bool -> 整数类型,true是1,false是0
    • 整数类型 -> bool,非零为1
    • float -> int,只保留整数部分,发生精度丢失
    • int -> float:小数部分为0
    • 给无符号类型赋值,如果超出它表示范围,则结果是初始值对无符号类型能表示的总数取模后的余数
    • 给有符号类型赋值,如果超出了它的表示范围,则结果是未定义的(undefined)。此时,程序可能继续工作,也可能崩溃。
  9. C++中,问号也需要使用转义字符。

三 运算符

  1. 在基本数据类型中,除了void之外,其他数据类型都可以进行算数运算。

  2. 在进行算数运算中,精度不同的数据类型进行运算时,运算结果的数据类型取精度大的那个数据类型。

  3. 取余运算符%的两个操作数必须是整数类型

  4. 赋值运算的规则:

    • 赋值运算符=左侧必须是可修改的左值
    • 如果赋值运算符左右两侧的数据类型不同,就把右侧的对象转换成左侧对象的数据类型
    • C++ 11新标准提供了一种新的语法:用花括号{}括起来的数值列表,可以作为右值。这样就可以非常方便的给数组赋值了。
    • 赋值操作可以连续,从右到左依次执行。
    • 赋值运算符优先级很低,一般会执行其他运算符,最后进行赋值
  5. 短路求值:||的左侧为真,则会跳过右侧判断; &&左侧的结果为假,则会跳过右侧判断。可以用于特定左侧条件下执行右侧语句,能否代替if条件语句?

  6. 在C++中,如果要进行位移运算,那么长度小于int的数据类型会默认提升到int类型,运算的结果也是int类型。

  7. 在左移操作时,默认左移的位数是小于32的,如果大于等于32,则左移的位数会对32取模。例如左移34位相当于左移2位。

  8. 在自动类型转换的时候,一般会将长度较小的类型转换到长度较大的类型,从而避免精度丢失。但是在将浮点型转换为整数类型是,会丢弃小数部分,只保留整数部分,造成精度丢失。

  9. C++中强制类型转换的语法有三种:

    int total = 20, num = 6;
    // 方式一:c语言风格
    double avg = (double)total / num;
    // 方式二:c++风格
    double avg = double(total) / num;
    // 方式三:c++强制类型转换运算符
    double avg = static_cast(total) / num;
    

四 流程控制

  1. switch-case中,每个case一般会配合break使用,表示跳出当前switch。如果case中没有加break,则会继续执行其他case中的语句,即使case没有匹配,直到遇见break。

    switch(变量){
        case 值1:
            ...;
            break;
        case 值2:
            ...;
            break;
        ...
        default:
            ...;
    }
    
  2. C++中的for循环和Java一样,都有两种方式:普通for循环和增强for循环。

  3. C++中有goto语句,只需要在某个代码上面做个标记,就可以使用goto进行跳转。goto语句非常灵活,但是也非常危险,很容易造成死循环。

五 复合数据类型

  1. 数组的定义

    int a1[10];
    const int n = 4;
    double a2[n];
    int a3[4] = {1, 2, 3, 4};
    double a4[] = {1.1, 2.2, 3.3}; // 自动推断长度
    short a5[10] = {3, 6, 9};
    short a6[2] = {1, 2, 3}; // 报错,初始值太多
    int a6[4] = a3; // 报错,不能用另一个数组对数组进行赋值
    
  2. 在定义数组时,数组的长度必须为常量,如果是变量会报错。(Java不会)

  3. 在方法中定义的数组的长度必须大于0

    int a[0]; //会报错,数组元素必须大于0
    
  4. 空数组可以定义,但仅限于类或者结构体中

  5. 在Visual studio中,对为定义的局部变量赋值为0xcc。如果直接打印,则会打印出"烫烫烫烫…"。

  6. C++中数组的长度是如何获取的?

    数组所占空间 = 数据类型所占空间大小 * 元素个数
    元素个数 = 数组所占空间 / 数据类型所占空间大小
    
  7. C++中,如果对数组进行越界访问,并不会报错,而是接着数组的最后一个元素的内存位置继续向后访问。因此可能访问到其他程序正在占用的内存。所以在C++中使用数组,要严格限制访问的下标在数组范围之内。

  8. 多维数组初始化

    int ia[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int ia2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
    int ia3[][4] = {1, 2, 3, 4, 5, 6, 7}; // 自动推断
    
  9. vector详解(相当于Java中的List)

    • 初始化方式:

      // 默认初始化
      vector v1;
      // 拷贝初始化
      vector v2 = {'a', 'b', 'c'};
      // 等号可以省略
      vector v3{'a', 'b', 'c'};
      // 直接初始化,定义一个初始长度为5的容器,默认初始化值为0
      vector v4(5);
      // 直接初始化,定义一个长度为5的容器,默认初始化值为100
      vector v5(5, 100);
      
    • 访问元素:可以通过下标访问。越界访问会报错退出。

    • 添加元素:

      v5.push_bask(69);
      
  10. 除了vector之外,C++ 11还提供了一个array模板类,它跟数组更加类似,长度是固定的,但更加方便,更加安全。所以在实际应用中,一般推荐对于固定长度的数组使用array不固定长度的数组使用vector

  11. 字符串详解

    • 初始化方式

      // 默认初始化
      string s1;
      // 拷贝初始化
      string s2 = s1;
      string s3 = "Hello world";
      // 直接初始化
      string s4("Hello world");
      string s5(5, 'a');
      
    • 访问字符:可以通过下标进行访问

      cout << s4[2] << endl; // 输出l
      s4[0] = 'h'; // 字符串变为 hello world
      cout << s4[s4.size() - 1] << endl;
      
    • 字符串拼接

      string str1("hello"), str2 = "world";
      string str3 = str1 + str2;
      
      • 两个字符串对象可以相加,一个字符串对象和一个字符串常量可以相加,两个字符串常量不可以相加。
      • 原因是C++中的标准模板类string为了和c语言中的字符串进行兼容。因此在c++/c中,字符串常量在底层的存储形式是char数组。因此+运算符重载的条件就是必须基于一个string对象。在多个string对象和多个字符串常量同时做相加时,需要满足左结合律,即+左边必须是一个string对象。
    • 字符串比较

      • 在C++中,在不使用指针的情况下,即使是像string str2 = str1;这样的语句,str1 与 str2都是不同的对象,在内存中的地址也是不一样的。
      • 在C++中,== 、 <= 、>=号都可以进行字符串比较,但比较的字符串的内容,而不是字符串地址。
    • 字符数组(c语言风格字符串)

      • 在c语言中,并没有字符串类型。字符串都是以char[]的形式保存的。

      • 在c语言中,可以通过如下形式定义字符串

        char str1[] = {'a', 'b', 'c', 'd', 'e', 'f', '\n'};
        char str2[] = "abcdef";
        
      • 一个字符串的结束标识是\0,在进行打印的时候,printf和cout总是会在遇到\0时结束打印。使用char str2[] = "abcdef";方式定义字符串是,会默认在末尾增加一个隐藏的\0。

      • C++为了兼容c语言,字符串常量都是以char[]形式进行存储的。

      • 一般推荐直接使用string,尽量少使用字符数组来表示字符串。

  12. 命令行输入输出

    • cin >> str:忽略开头的所有的空白符,从第一个非空白符开始读取,直到遇到下一个空白符。即cin每次读取的是一个单词。下一个空白符之后的内容还会被保存在输入缓存中。
    • getline(istring input,string str):从输入流input中读取一行输入保存到str中,直到遇到换行符,并丢弃该换行符。
    • cin.get():读取输入缓存中的一个字符。还有一个重载函数,cin.get(char[] str, int length):读取length长度的输入,保存在字符数组str中,用的比较少。
  13. 文件输入输出

    • ifstream:用法基本和cin一致,只是操作对象从命令行变更为文件
      • >>:从文件中读取一个单词
      • getline(ifstream, str):读取一行
      • get():读取一个字符
  14. 结构体的 定义 必须在 使用 之前。

  15. 枚举:

    • 枚举类型内部只有有限个名字,他们各自代表了一个常量,被称为枚举量

    • 默认情况下,会将整数值赋值给枚举量,默认从0开始,每个枚举量依次加1

      enum week{
          // 分别对应着 0 ~ 6的常量
          Mon, Tue, Wed, Thu, Fri, Sat, Sun
      }
      
    • 可以通过对枚举量赋值,显式的设置每个枚举量的值。

    • 如果直接用一个整型值对枚举类型赋值,将会报错,因为类型不匹配;

    • 可以通过强制类型转换,讲一个整型值赋给枚举对象;

    • 最初的枚举类型只有列出的值时有效的;而C++通过强制类型转换,允许扩大枚举类型合法值的范围。不过一般使用枚举类要避免直接强转赋值。

  16. 指针

    • 概念:指针顾名思义,就是"指向"另外一种数据类型的复合类型。指针是C/C++中一种特殊的类型,他所保存的信息,其实是另外一个数据对象在内存中的’'地址"。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法。指针类型的长度是固定的,与操作系统寻址空间有关。

    • 所谓64位系统和32位系统,说的是内存寻址空间的长度是64位bit和32位bit。在64位系统中,内存寻址空间从0 ~ (2^64 - 1)。因此指针中可以保存的最大的地址是2^64 - 1,共需要8个字节来存储地址信息。而在32位系统中,内存寻址空间从0 ~ (2^32 - 1),因此只需要4个字节来存储地址信息。

    • 指针指向的地址,指的是指向对象的首地址。因为数据类型的长度是不同的,因此知道是何种类型的指针和首地址后,解引用操作就可以通过首地址加偏移量获取指向对象的值。

    • 指针类型定义如下:

      int a = 10;
      int* p1 = &a;
      
      long l = 20;
      long* p2 = &l;
      
      // 不可以将p2指针指向 &a,因为类型不匹配
      // 指针类型指的就是int* 、 long* 这些类型,而p1 、p2 是指针类型的变量
      
    • 解引用

      // 可以使用* 对指针类型的变量进行解引用
      cout << p1 << endl; 	// 输出00000059362FF7B4,是变量a的地址
      cout << *p1 << endl; 	// 输出10
      *p1 = 20;
      cout << *p1 << endl; 	// 输出20
      
    • 一些特殊指针

      • 无效指针:没有初始化的指针,这些指针指向的地址是不确定的,因此使用这些指针是非常危险的!一般来说编译器是不会让无效指针通过编译的。无效指针也叫做野指针,不能使用野指针。

      • 空指针:先定义了一个指针,但还不知道它要指向哪个对象,这是可以把它初始化为"空指针"。有三种方法定义空指针:

        // 三种方式本质上都一样
        int* np = nullptr;
        np = NULL;
        np = 0;
        
      • void* 指针:表示该类型的指针可以指向任意类型的数据对象。但无法解引用,因为不知道指向的数据类型的长度。一般void* 类型的指针只用来存放指针的地址,不做解引用操作。

      • 二级指针:指向指针的指针,即保存的是一级指针的地址。对二级指针进行解引用将会得到一级指针的值,一级指针的值本质上也是一个地址,需要在用一次解引用后获得所指向的基本数据类型的值。也就是说,解引用符*操作地址 可以获得对应地址的值。

    • 指向常量的指针和指针常量

      • 指向常量的指针:顾名思义,这个指针是个变量,只是指向的数据类型是个常量。指针本身是可以变的,即可以指向其他的常量。但指向的常量是不可以变的。即解引用后无法更改内容。
      • 指针常量:指针是常量,指向的数据类型是变量。指针本身存放的地址是固定的,这个地址中存放的值是变量,是可变的。
      • 指向常量的指针常量:意思就是说指针本身是常量,指向的数据类型也是常量。究极之不可变。一旦定义,指针本身的保存的地址不能变,改地址中的对象也不能变。
      int a = 100;
      int b = 200
      const int num1 = 10;
      const int num2 = 20;
      // 指向常量的指针
      const int* pc = &num1;
      cout << *pc << endl;	// 输出10
      pc = &num2;
      cout << *pc << endl;	// 输出20
      // *pc = 25;			// 错误,无法改变常量
      // 指针常量
      int* const cp = &a;
      cout << *cp << endl;	// 输出100
      *cp = 150;
      cout << *cp << endl;	// 输出150
      // cp = &b;				// 错误,无法修改常量
      // 指向常量的指针常量
      const int* const cpp = &num1;
      // cpp = &num2;			// 错误,无法修改常量
      // *cpp = 15;			// 错误,无法修改常量
      
    • 指针和数组

      • 数组名本质上就是一个指针,指向数组中第一个元素的首地址。不能对数组进行拷贝赋值,本质上就是因为不能将一个地址赋值值给数组。
      • 可以对数组名进行加运算。会根据数组的类型进行寻址。本质上就是跳过一些数组元素指向后面的数组元素。
    • 指针数组和数组指针

      • 指针数组:一个数组,里面保存的都是指针
      • 数组指针:一个指针,指向一个数组,该数组指针变量的内容是指向的数组的首地址。在解引用的时候会根据这个数组的长度来确定内存地址取值范围,从而生成一个数组对象。数组对象名指向的是数组中第一个元素,因此保存的值是数组中第一个元素的首地址;事实上数组的首地址和数组中第一个元素的首地址是相同的。编译器会根据类型和地址来确定解引用的结果
      int arr[5] = {1, 2, 3, 4, 5};
      // 指针数组
      int* pa[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
      // 数组指针
      int (*ap) [5];
      
      ap = &arr; // arr是指向arr数组的第一个元素的指针,arr的值是数组中第一个元素的首地址
      cout << ap << endl;
      cout << arr << endl;
      
    • 关于数组名和指针的一些思考关于C++数组名和指针的一些思考

  17. 引用:定义变量的别名,类似于快捷方式。

    • 引用的定义

      int a = 10;
      int b = 25;
      // 一旦定义引用的对象,之后就不可以更改
      int& ref = a;
      
      // int& ref; 报错!引用必须被初始化。
      // int& ref = 10; 报错!变量的引用必须指向对象,而不是字面值。
      
      // ref = b;并不代表引用指向b,而是ref的值被修改为25,即a被修改为25。
      ref = b;
      
    • 引用时内存中变量的别名,本身并不占据内存空间,一旦定义引用,就与指向的变量是共同的内存地址。

    • 引用的引用:套娃,就是别名的别名,本身还是指向同一个对象。

    • 常量引用:顾名思义,就是对常量的引用。但是在初始化时,有时可以不必指向常量。对常量引用的初始化要求非常宽松。

      const int ci = 20;
      int i = 30;
      const int& cref = ci;
      const int& cref2 = i;		// 正确,常量引用可以直接引用变量
      const int& cref3 = 10;		// 正确,常量引用可以直接引用字面量
      
      double d = 3.14;
      const int& cref4 = d;		// 正确,此时d被隐式转化为int类型的值 3,cref4引用了字面量3
      
      // cref2 = 10;				错误!虽然cref2引用变量,但常量引用不能修改值
      
    • 引用可以看做是指针常量解引用的语法糖,但本质上并不是指针常量的解引用,因为指针常量需要占据内存,引用不占据内存。

    • 可以有对指针的引用

      int a = 10;
      int* ptr = &a;
      int*& pref = ptr;
      // 操作pref就是操作ptr
      cout << *pref << endl; 		// 输出10;
      
    • 没有指向引用的指针,因为指针保存的是内存地址,而引用只是别名,并不进行存储,因此没有地址。

  18. C++中的箭头运算符 ->:是解引用可访问成员两个操作的结合;这样就可以很方便的表示"取指针所指向内容的成员"

    Student s1 = Student("阿秋", 25);
    Student* p = &s1;
    
    // 下面两个语句是等价的
    cout << (*p).getName() << endl;
    cout << p -> getName() << endl;
    
  19. C++中的对象赋值:就是把对象的内容复制过去,对象的地址不发生改变。浅谈C++和Java中对象的等号赋值

六 函数

  1. 全局变量和局部变量:每个变量都有其作用域,一个作用域就是一对{},在{}内定义的变量,作用域只在该{}内。定义在{}之内的变量由于就称为局部变量。而定义在所有{}之外的变量,它的作用域是全局可见的,被称为全局变量

  2. 自动对象和静态对象

    • 自动对象:在平常代码中定义的普通局部变量,生命周期为:在程序执行到变量定义语句时创建,在程序运行到当前代码块末尾时销毁。这样的对象被称为自动对象。方法中的形参也是一种自动对象。对于自动对象来说,它的生命周期和作用域是一致的。
    • 静态对象:如果希望延长一个局部变量的生命周期,让他在其作用域之外依然存活,可以在定义局部变量时加上static关键字。这样的对象叫做局部静态对象。注意,局部静态对象依然只有局部的作用域,在作用域之外依然是不可见的。但它的生命周期贯穿了整个程序运行过程,只有在程序结束时才被销毁,这一点与全局变量类似。静态对象如果不在代码中做初始化,基本数据类型会被默认初始化为0值。
  3. 函数的声明

    • 在C++代码的执行过程中,对于变量、对象和函数的使用,是严格按照县声明再使用的方式的。也就是说,你不可以使用一个在这个语句之后才声明的变量、对象或函数。注意,对于全局对象和全局变量而言,如果在声明时没有初始化,则会进行默认初始化;对于函数而言,是先声明再使用,而不是先定义在使用,因此可以先声明后定义
    • 可以在main()函数之前声明需要用到的函数,然后在任意其他位置进行定义。但一般而言,会使用头文件的来声明函数。
    • #pragma once:在.h头文件中,可以进行结构体、类和函数的定义。头文件是可以在多个文件中被引用的。因此为了避免盖头文件中的内容被多次定义,可以在.h文件中声明#pragma once,表示里面定义的内容在全局只会定义一次。同时#pragma once还可以表示在嵌套包含时,.h文件只会被包含一次。
  4. 参数传递:参数传递的本质,其实就是使用实参对形参进行初始化赋值。

    • 传值:将实参的值赋值给形参,实参和形参在内存中是两份实体,并不相互影响。

    • 传引用:将形参定义为引用,实参也是引用。将引用传递给引用,其实就是引用的引用,所指向的对象是相同的。

      • 传引用的好处是:可以对函数外部数据对象进行更改可以避免数据复制

      • trick:在定义引用类型的参数的时候,如果不会更改引用对象本身的内容,那么可以把参数类型定义为常亮引用。这样做的好处是能够扩大传参的范围。因为引用变量是无法用字面值初始化的,常亮引用是可以用字面值初始化的。

      void fun(const string& str1, const string& str2) {
      	cout << str1 << endl;
      	cout << str2 << endl;
      }
      
      void fun2(string& str1, string& str2) {
      	cout << str1 << endl;
      	cout << str2 << endl;
      }
      
      int main() {
      
          // 正确
      	fun("hello", "world");
      
          // 报错,无法用常亮初始化引用变量
      	fun2("hello", "world");
      
      	return 0;
      }
      
    • 传数组:有三种方式传数组,前两种会把传入的数组退化成一个指针。一般建议使用传数组引用的方式。

      void fun1(int* arr) {
      	cout << (sizeof arr) << endl;
      }
      
      void fun2(int arr[5]) {
      	cout << (sizeof arr) << endl;
      }
      
      void fun3(int (& arr) [5]) {
      	cout << (sizeof arr) << endl;
      }
      
      int main() {
      
      	int arr[] = { 1, 2, 3, 4, 5 };
      	cout << sizeof arr << endl;
      
      	fun1(arr); 	// 输出8
          fun1(arr);	// 输出8
          fun1(arr);	// 输出20
      
      	return 0;
      }
      
  5. 返回类型

    • typedef:用于自定义一个类型的别名。在函数返回值比较复杂的时候,可以使用自定义类型别名来简化。
    • 尾置返回类型:在函数声明的时候,可以在函数前使用auto关键字,在函数尾部使用 -> 数据类型 的方式来声明函数。此时函数的返回类型就是尾部声明的类型。

七 函数高阶

  1. 内联函数:函数声明前 使用 inline关键字,表示该函数在调用时,直接拷贝函数体内的代码到调用处,而不做函数调用。省去了保存上下文和函数入栈等操作。在某种程度上可以优化运行效率。但最近研究表明,inline函数的功能已经发生了改变,编译器会自动进行内联优化,如果仅仅是为了优化效率,则不建议使用inline。现代C++里,inline被理解成:它通知编译器,这个符号可能会在多个翻译单元中重复出现。大坑inline函数

  2. 函数参数默认值

    • 在C++中,可以定义函数参数的默认值,直接在函数定义的时候在参数列表后面使用等号为参数设置默认值即可。在调用函数进行传参时,python是可以显式的指定参数名从而为指定形参赋值的。与python不同,C++无法跳过某些默认值进行之后的参数设置,必须严格按照参数顺序进行传参。

    • 在Java中无法设置函数参数默认值,但是可以通过函数重载来达到相同的目的。

  3. 函数重载

    • 和Java一样,C++支持函数重载。而C语言和python并不支持函数重载。在python中,后定义的函数会覆盖先定义的函数。

    • const类型的函数重载

      • 顶层const:const修饰参数本身,与不带const修饰的参数在接收参数的功能上是一致的。无法进行函数重载。
      • 底层const:一般用于指针和引用的修饰,修饰的是参数所指向/引用的数据类型。可以进行重载。
    • 函数匹配

      void fun(int x){
          cout << 1 << endl;
      }
      void fun(int x, int y){
          cout << 2 << endl;
      }
      void fun(double x, double y = 1.5){
          cout << 3 << endl;
      }
      
      int main(){
          // 输出3
          fun(3.14);
          // 发生二义性调用。报错!有多个重载函数fun实例与参数列表匹配
          fun(3.14, 10);
      }
      
      • 参数匹配时,参数数量要相等,参数类型也需要相同或者可以通过隐式类型转换的。
      • 最佳匹配:不需要进行类型转换的优先匹配,或者需要最少次数的隐式类型转换。
      • 多参数匹配:按照参数类型精确匹配优先,或者有某一个可行的函数都不必其他函数差,并且最少有一个参数匹配更优,那么它就是最佳匹配。
      • 二义性调用:在检查所有重载函数之后,发现有多个可行函数不分优劣,无法找到一个最佳匹配,编译器就会直接报错,这中情况被称为二义性调用
    • 在Java中,函数参数匹配时,只可以发生向下的隐式类型转换,例如int类型传递给double类型。无法在丢失精度的情况下发生类型转换。并且int型优先匹配为long,而不是double。

    • 函数重载必须在同一作用域中。如果在不同作用域中声明同名函数,内层的函数会覆盖外层的声明或定义的其他同名不同参数的函数

      void fun(int i){
          cout << i << endl;
      }
      void fun(double i){
          cout << i << endl;
      }
      void fun(string i){
          cout << i << endl;
      }
      
      int main(){
          void fun(int i);
          // 可行,调用fun(int i)
          fun(10);
          // 可行,调用fun(int i),发生参数类型隐式转换
          fun(3.14);
          // 报错!无法调用fun(string i)
          fun("hello");
      }
      
  4. 函数指针和函数引用

    函数其实也是有类型的,函数的类型由其返回值和参数类型共同决定,与函数名和参数名无关。

    void testDefault(double i) {
        cout << 1 << endl;
    }
    
    void testDefault(long i) {
        cout << 2 << endl;
    }
    
    int main() {
        // 定义函数指针
        void (*fp) (long) = nullptr;
        // 以下两种赋值语句等价
        fp = testDefault;
        fp = &testDefault;
        
        // 以下两种调用方式等价,输出3
        (*fp)(3);
        fp(3);
        
        // 定义函数引用  
        void (&fr) (long) = testDefault;
        // 也可以调用函数,输出3
        fr(3);				
    }
    
  5. 函数指针和函数引用作为函数的参数。感觉比较离谱

    • 作用基本相同,唯一不同的是函数指针的引用作为参数时,无法使用函数名作为参数传入,此时需要定义为const

    • python中也有函数作为参数的概念,在Java中可以使用接口和匿名内部类的方式来实现。不过C++中的语法还是太离谱了,很多种方式都可以。

    • typedef decltype(funName) newName:提取funName函数的类型,定义函数类型为新的名称newName

    • 疯了!下面的代码除了注释的部分,其他都没问题,请自行理解

    void testDefault(long i) {
        cout << i << endl;
    }
    // 以下几种定义方式相同,可以省略指针符号*
    void fun1(long i, void (*fp) (long)) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun2(long i, void fp(long)) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    // 使用类型别名,以下方式相同,函数名本身就被编译器解析为指针
    
    typedef void (*Funcp)(long);
    typedef void Func(long);
    typedef decltype(testDefault) Func;
    
    
    void fun3(long i, Funcp fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun4(long i, Funcp& fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun5(long i, const Funcp& fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun6(long i, Func fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun7(long i, Func& fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    void fun8(long i, Func* fp) {
        // 以下两种调用方式相同,可以省略解引用符号*
        (*fp)(i);
        fp(i);
    }
    
    
    Func& fun9() {
        return testDefault;
        // return &testDefault;     报错
    }
    
    Func* fun10() {
    
        return testDefault;
        return &testDefault;
    }
    
    Funcp fun11() {
    
        return testDefault;
        return &testDefault;
    }
    
    Funcp& fun12() {
    
        Funcp f = testDefault;
        return f;
    }
    
    int main() {
        // 以下两种调用方式相同,可以省略去地址符号&
        fun1(10, testDefault);
        fun1(10, &testDefault);
    
        fun2(10, testDefault);
        fun2(10, &testDefault);
    
        fun3(10, testDefault);
        fun3(10, &testDefault);
    
        Funcp f = testDefault;
        Funcp& ref = f;
    
        fun4(10, f);         
        fun4(10, ref);
        //fun4(10, testDefault);    报错,无法使用字面量初始化一个引用
        //fun4(10, &testDefault);    报错,无法使用字面量初始化一个引用
    
        fun5(10, &testDefault);
        fun5(10, testDefault);
    
        fun6(10, &testDefault);
        fun6(10, testDefault);
    
        fun7(10, testDefault);
    
        fun8(10, &testDefault);
        fun8(10, testDefault);
    
        fun1(10, fun9());
        fun1(10, fun10());
        fun1(10, fun11());
        fun1(10, fun12());
    
        fun2(10, fun9());
        fun3(10, fun10());
        // fun4(10, fun11());   报错,字面量无法赋值引用变量
        fun5(10, fun12());
    
        fun4(10, fun12());
    }
    
    • 函数指针作为返回类型:直接使用函数名是不可以的,必须使用函数指针或函数引用才可以。
    • 综上所示,函数名作和数组一样。在直接使用的时候可以当做一个指针,但本身并不是指针。并且在作为函数参数传入时,会退化为一个普通指针。
    • 疯了疯了…

八 面向对象

你可能感兴趣的:(C++,c++,学习)