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

C++学习笔记--黑马程序员

一维数组

数组就是一个集合,里面存放了相同类型的数据元素。

  • 特点1:数组中的每个数据元素都是相同的数据类型
  • 特点2:放在一块连续的内存空间中。
    一维数组定义的三种方式:
  1. 数据类型 数组名[数组长度];
  2. 数据类型 数组名[数组长度] = {值1,值2,...}
  3. 数据类型 数组名[] = {值1,值2,...}
    一维数组名称的用途
  4. 可以统计整个数组在内存中的空间大小(占用多少byte)
  5. 可以获取数组在内存中的首地址
#include <iostream>
using namespace std;

int main()
{
    int arr1[] = {1, 2, 3};

    for (int i = 0; i < 3; i++) // 3代表数组中元素的个数
    {
        cout << arr1[i] << endl;
    }
    return 0;
}

示例程序1:数组元素逆序(数组元素首位互换)

#include <iostream>
using namespace std;

void Reverse(int a[], int n)
{
    // i为首元素,j为尾元素
    for (int i = 0, j = n - 1; i < j; i++, j--)
    {
        // 首尾交换
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

int main()
{
    int arr[] = {1, 3, 2, 5, 4};
    int n = sizeof(arr) / sizeof(int);
    Reverse(arr, n);
    for (int i = 0; i < n; i++)
    {
        cout << arr[i] << ",";
    }
    cout << endl;
    return 0;
}

示例程序2:冒泡排序

  1. 比较相邻元素的大小,如果第一个比第二个大,就交换他们两个
#include <iostream>
using namespace std;

void Bubble_Sort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++) // 外循环控制循环次数,n个数需要排n-1次
    {
        bool flag = true;                   // 没有排序为true,本趟有排序为false
        for (int j = 0; j < n - 1 - i; j++) // 每排1次,趟数减1
        {
            if (arr[j] > arr[j + 1])
            {
                flag = false;
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
        if (flag) // 已完成全部排序,提前结束
        {
            break;
        }

        // 输出每次排序的结果
        cout << "第" << i + 1 << "次:";
        for (int k = 0; k < n; k++)
        {
            cout << arr[k] << ",";
        }
        cout << endl;
    }
}

int main()
{
    int arr[] = {3,2,1};
    int n = sizeof(arr) / sizeof(int);
    Bubble_Sort(arr, n);
    return 0;
}

二维数组

二维数组就是在一维数组上,多加一个维度。
二维数组的定义方式:行数可以省略,列数不能省略

#include <iostream>
using namespace std;

int main()
{
    // 数组类型  数组名[行数][列数]
    int arr1[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 推荐这种命名方式
    int arr2[2][3] = {1, 2, 3, 4, 5, 6};
    int arr3[][3] = {1, 2, 3, 4, 5, 6};

    // 遍历二维数组,外层循环打印行数,内层循环打印列数
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            cout << arr3[i][j] << " ";  // 每一行的元素
        }
        cout << endl;
    }
    return 0;
}

二维数组数组名称

二维数组名称的作用与一维数组类似。

  1. 可以查看二维数组所占内存空间
  2. 可以获取二维数组首元素的地址
    示例程序:统计每个人的总和分数
#include <iostream>
using namespace std;

int main()
{
    int score[3][3] = {{100, 100, 100}, {90, 90, 90}, {80, 80, 80}};
    for (int i = 0; i < 3; i++)
    {
        int sum = 0;
        for (int j = 0; j < 3; j++)
        {
            sum += score[i][j]; // 每一行的总和
        }
        cout << "stu" << i+1 << ": " << sum << endl;
    }
    return 0;
}

函数

函数的声明:声明可以写多次,但是函数的定义只能有一个

  • 值传递:函数调用时,实参将数值传入给形参。
  • 注意:进行值传递时,函数的形参发生改变,并不会影响实参
    示例1:值传递,函数修改的是副本的值,实参的值不变。
#include <iostream>
using namespace std;

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
    cout << "a= " << a << ",b= " << b << endl;
}

int main()
{
    int num1 = 1;
    int num2 = 2;
    swap(num1, num2);                                     // 进行交换
    cout << "um1= " << num1 << ",num2= " << num2 << endl; // 没有发生改变,因为值传递时,形参不影响实参

    return 0;
}

示例2:地址传递,把实参的存储地址传给形参,形参的变化会影响实参

#include <iostream>
using namespace std;

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
    cout << "a= " << *a << ",b= " << *b << endl;
}

int main()
{
    int num1 = 1;
    int num2 = 2;
    swap(&num1, &num2);                                     
    cout << "um1= " << num1 << ",num2= " << num2 << endl; 

    return 0;
}

示例3(重点,常用):引用传参,可以使得对形参的任何操作都能改变相应的数据,又使函数调用方便。与指针传参的效果一样,不过更加易于阅读

  • 当传递的数据量较大时,用引用传递参数的时间和空间效率更好。
  • 引用相当于const *
  • 具体是使用引用传递还是值传递,取决于是否想要实参发生改变。
#include <iostream>
using namespace std;

void swap(int &a, int &b) // 引用传参
{
    int temp = a;
    a = b;
    b = temp;
    cout << "a= " << a << ",b= " << b << endl;
}

int main()
{
    int num1 = 1;
    int num2 = 2;
    swap(num1, num2);
    cout << "um1= " << num1 << ",num2= " << num2 << endl;

    return 0;
}

函数的分文件编写

作用:让代码结构更加清晰
函数分文件编写一般有4个步骤:

  1. 创建后缀名为.h的头文件,写函数的声明
  2. 创建.cpp的源文件,写函数的定义

指针

2023.5.23
指针就是地址。

  • 在32位操作系统下,指针占用4个字节,64位系统占用8个字节
  • 空指针:指针变量指向内存中编号为0(NULL)的空间。用途:初始化指针变量。注意:空指针指向的内存是不可以访问的
  • 野指针:指针变量指向非法的内存空间

const修饰指针

  1. const修饰指针:const int *p常量指针
  2. const修饰常量:int * const p指针常量
  3. const即修饰指针,又修饰常量const int * const a
    技巧:看const后面是指针还是常量,是指针就是常量指针,是常量就是指针常量

指针和数组

#include <iostream>
using namespace std;

int main()
{
    int arr1[] = {1, 2, 3, 4, 5};
    int *p = arr1;            // 数组名就是数组首地址,*p代表第一个元素arr[0]
    cout << *(p + 1) << endl; // 相当于arr[1] 
    return 0;
}

数组作为函数参数进行传递时,会退化为指针,必须再传入数组的有效长度,才能正常使用。

结构体

结构体属于用户自定义的数据类型,允许用户存储不同的数据类型。
通过结构体创建变量的方式有三种:

  1. struct结构体名 变量名
  2. struct结构体名 变量名 = {成员值1,成员值2,…}
  3. 定义结构体时顺便创建变量
    将结构体作为参数向函数中传递:值传递(形参不会影响实参的值)、地址传递。
    值传递相当于拷贝一份实参的数据
    地址传递,将函数中的形参改为指针,可以减少内存空间,而且不会复制新的副本出来。
    总结:如果不想修改主函数中的数据,用值传递,反之用地址传递。
    使用const可以防止误操作!
    结构体案例1:设计一个英雄的结构体,包括姓名,年龄,性别。创建一个结构体数组,通过冒泡排序法,将数组中的年龄进行升序,并打印结果。
    设计步骤:
  4. 设计英雄结构体
  5. 创建数组存放5名英雄
  6. 对数组进行排序,按照年龄进行升序排序
  7. 将排序后的结果打印输出
#include <iostream>
#include <string>
using namespace std;

struct Stu
{
    string name;
    int age;
    string sex;
};

Stu people[5] = {
    {"刘备", 23, "男"},
    {"关羽", 22, "男"},
    {"张飞", 20, "男"},
    {"赵云", 21, "男"},
    {"貂蝉", 19, "女"},
};

// 冒泡排序传入结构体的数组
void BubbleSort(struct Stu people[], int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - 1 - i; j++)
        {
            if (people[j].age > people[j + 1].age)
            {
                Stu temp = people[j]; // 创建一个临时结构体
                people[j] = people[j + 1];
                people[j + 1] = temp;
            }
        }
    }
}

int main()
{
    for (int i = 0; i < 5; i++)
    {
        cout << "姓名:" << people[i].name << ",年龄:" << people[i].age << ",性别:" << people[i].sex << endl;
    }

    BubbleSort(people, 5);
    cout << endl; // 换行,打印排序后的结果
    for (int i = 0; i < 5; i++)
    {
        cout << "姓名:" << people[i].name << ",年龄:" << people[i].age << ",性别:" << people[i].sex << endl;
    }
    return 0;
}

运行结果:

姓名:刘备,年龄:23,性别:男
姓名:关羽,年龄:22,性别:男
姓名:张飞,年龄:20,性别:男
姓名:赵云,年龄:21,性别:男
姓名:貂蝉,年龄:19,性别:女

姓名:貂蝉,年龄:19,性别:女
姓名:张飞,年龄:20,性别:男
姓名:赵云,年龄:21,性别:男
姓名:关羽,年龄:22,性别:男
姓名:刘备,年龄:23,性别:男

程序的内存模型(要会)

C++程序在执行时,将内存分为4个区域:

  1. 代码区:存放函数体的二进制代码,由操作系统进行管理
  2. 全局区:存放全局变量、静态变量、常量(const修饰的变量、字符串常量),该区域的数据在程序结束后由操作系统释放
  3. 栈区:由编译器自动分配释放,存放函数的参数值、局部变量
  4. 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收,主要利用new在堆区开辟内存

C++中的引用

引用的基本使用

  • 作用:给变量起别名
  • 语法:数据类型 &别名 = 原名
  • int &a = 10; // 不合法
    引用的注意事项:
  • 引用必须初始化,在初始化之后,不可改变
  • 引用的本质在C++内部实现是一个指针常量(指向的地址不能被修改)int * const a

引用做函数的参数

  • 作用:函数传参时,可以利用引用的技术让形参修饰实参
  • 优点:可以简化指针修改实参
void add(int &a, int &b){
   int c = a+b;
}

函数提高

函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值 函数名 (参数=默认值){}
注意事项:

  1. 如果在自定义函数中,在某个位置有默认参数,那么这个位置后面的参数(从左到右),都要有默认值
  2. 如果函数的声明有默认值,那么函数实现的时候就不能有默认参数,声明和实现只能有一个有默认参数
int add(int a, int b = 10); // 函数声明

int add(int a, int b)  // 函数实现
{
   return a + b;
}
  1. 在函数调用时,如果给默认参数传值了,则不用默认值
#include <iostream>
using namespace std;

int add(int a, int b = 10)
{
    return a + b;
}

int main()
{
    int num1 = 10;
    int c = add(num1);  // 输入一个参数即可
    cout << c;

    return 0;
}

函数占位参数

C++中函数的形参列表里可以有占位参数,用来占位,调用函数时必须填补该位置
语法:返回值类型 函数名(数据类型) {}
目前阶段的占位参数,还用不到,后面的课程中会用到。
占位参数也可以有默认参数void func(int a, int = 10)

#include <iostream>
using namespace std;
void func(int a, int) // 最后一个参数用于占位
{
    cout << "test" << endl;
}

int main()
{
    func(10, 20); // 占位参数必须填补
    return 0;
}

函数重载

  • 作用:函数名可以相同,提高复用性
  • 函数重载满足条件
    • 同一作用域下
    • 函数名相同
    • 函数的参数类型不同,或者个数不同,或顺序不同
      注意:函数的返回值不可以作为函数重载的条件
#include <iostream>
using namespace std;

// 函数重载需要函数都在同一个作用域下
void func()
{
    cout << "func 的调用" << endl;
}

void func(int a)
{
    cout << "func(int a) 的调用" << endl;
}

int main()
{
    func();
    func(10);
    return 0;
}

函数重载注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数
#include <iostream>
using namespace std;

// 1.引用作为重载条件
void func(int &a)
{
    cout << "func(int &a) 的调用" << endl;
}
void func(const int &a)
{
    cout << "func(const int &a) 的调用" << endl;
}

// 2.函数重载碰到默认参数
void func(int a, int b = 10)
{
    cout << "func(int a, int b = 10)的调用" << endl;
}
void func(int a)
{
    cout << "func(int a)的调用" << endl;
}

int main()
{
    int a = 10;
    // 1.引用作为重载条件
    // func(a); // 调用func(int &a)
    // func(10); // 调用func(const int &a)

    // 2.函数重载碰到默认参数
    // func(10);  // 当函数重载碰到默认参数时,出现二义性(两个函数都可以调用),报错,尽量避免这种情况
    func(10,20);  // 调用func(int a, int b = 10)

    return 0;
}

2023.5.25

类和对象

C++面向对象的三大特征:封装、继承、多态。
具有相同性质的对象,可以抽象为一个类,比如:人属于人类、车属于车类

  • 类中的属性和行为,我们统一称为成员
  • 类中的属性,又称为成员属性,成员变量
  • 类中的行为,又称为成员函数,成员方法
    在C++中structclass唯一区别在于默认的访问权限不同
  1. struct默认权限为公共
  2. class默认权限为私有

封装

封装是C++面向对象三大特性之一。
封装的意义:

  1. 将属性和行为作为一个整体,表现生活中的事物
  2. 将属性和行为加以权限控制
  3. 实例化:通过类创建一个对象的过程
    封装意义一:在设计类时,属性和行为给写在一起,表现实物。
    语法:class 类名 {访问权限: 属性/行为};
    封装意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制。
    类内:class {里面的内容,称为类内}
  4. public公共权限,类内可以访问,类外也可以访问
  5. protected保护权限,类内可以访问,类外不可以访问,继承:儿子可以访问父亲中的保护内容
  6. private私有权限,类内可以访问,类外不可以访问,protected的区别在于继承。继承:儿子不可以访问父亲中的私有内容。
    成员属性设置为私有
  • 优点1:将所有成员属性设置为私有,可以自己控制读写权限
  • 优点2:对于写权限,可以检测数据的有效性
    示例1:设计一个圆类,求圆的周长。
#include <iostream>
using namespace std;

const float PI = 3.14;

class Circle
{
private:
public:
    int R;
    float CalculateZC()
    {
        return 2 * PI * R;
    }
};

Circle c1; // 创建一个对象

int main()
{
    c1.R = 2;
    cout << "圆的周长:" << c1.CalculateZC() << endl;
    return 0;
}

示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号。

#include <iostream>
#include <string>
using namespace std;

class Student
{
private:
    string _name;
    int _number; // 学号
public:
    Student(string name, int number)
    {
        _name = name;
        _number = number;
    }

    void print()
    {
        cout << "姓名:" << _name << ",学号:" << _number << endl;
    }
};

int main()
{
    Student stu1("张三", 2023);
    stu1.print();
    return 0;
}

对象特性

  • 构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
  • 析构函数:在程序结束时系统自动调用,执行一些清理工作
    如果程序员不提供构造函数和析构函数,编译器会自动提供,不过编译器提供的构造函数和析构函数是空函数。

构造函数语法类名() {}

  1. 函数名与类名相同
  2. 构造函数没有返回值,也不写void
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时会自动调用构造函数,无需手动调用,而且只会调用一次
    析构函数:~类名() {}
  5. 函数名与类名相同,在名称前加上~
  6. 析构函数没有返回值,也不写void
  7. 析构函数不可以有参数,因此不可以发生重载
  8. 在程序结束前自动调用。
构造函数的分类及调用

两种分类方式:

  1. 按参数分为:有参构造和无参构造
  2. 按类型分为:普通构造和拷贝构造
    三种调用方式:
  3. 括号法
  4. 显示法
  5. 隐式转换法

深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑。

  1. 浅拷贝:简单的赋值拷贝操作
  2. 深拷贝:在堆区重新申请空间,进行拷贝操作。在析构函数中进行内存释放。

初始化列表

  • 作用:C++提供了初始化列表语法,用来初始化属性
  • 语法:构造函数():属性1(值1),属性2(值2){}
private:
    int _a;
    int _b;

public:
    Person(int a, int b) : _a(a), _b(b)
    {
        cout << "Person的构造函数调用" << endl;
    }

类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。

class A{}
class B{
    A a;
}

B类中有对象A作为成员,A为对象成员。
当创建B时,A与B的构造函数和析构函数的执行顺序是什么?
答:构造时先构造类对象,再构造自身,析构的顺序与构造相反。

静态成员

  • 静态成员:在成员变量和成员函数前加上关键字static
    • 静态成员变量:所有对象共享同一份数据,在编译阶段分配内存。类内声明,类外初始化
    • 静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量
      示例1:静态成员变量
  1. 所有对象都共享同一份数据(相当于全局变量)
  2. 编译阶段就分配内存,存储在内存中的全局区
  3. 类内声明,类外初始化操作
  4. 静态成员变量也可以设置访问权限
class Person
{
private:
public:
    static int a; // 静态成员变量,类内声明,类外初始化操作
};

int Person::a = 10; // 类中静态变量初始化

main()
{
    Person p1;
    cout << p1.a << endl;       // 通过对象访问成员变量
    p1.a = 20;
    cout << Person ::a << endl; // 通过类名直接访问成员变量
    return 0;
}

示例2:静态成员函数

  1. 所有对象共享同一个函数
  2. 静态成员函数只能访问静态成员变量
  3. 静态成员函数也可以设置访问权限

C++对象模型和this指针

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。

  • 静态成员变量不占用对象空间
  • 空对象占用空间为:1,因为C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
  • 每个空对象也应该有一个独一无二的内存地址
class Person
{
private:
public:
    int a;
    static int b; // 静态成员,不占用类中的内存空间
    void func(){}  // 非静态成员函数,不属于类对象上,不占用类的内存空间
    static void func2(){} // 静态成员函数,也不属于对象上
};

int Person::b = 0;
Person p1;

main()
{
    cout << sizeof(p1); // 输出结果4
    return 0;
}

总结:在类中,只有非静态成员变量占用内存空间

this指针

this指针的用途:

  1. 当形参和成员变量同名时,可用this指针来区分
  2. this是const指针,即常量指针,指向的地址不能被修改
  3. this只能在类的内部使用,可用访问类的所有成员(成员变量和函数)
  4. 友元函数没有this指针,因为友元不是类的成员

指向对象的指针

Worker w1;  // 创建一个对象
Worker *pt = &w1; // 创建指向对象的指针

const修饰成员函数

常函数:

  1. 成员函数后加const,称为常函数
  2. 常函数内不可以修饰成员属性
  3. 成员属性声明时加关键字mutable后,在常函数中仍然可用修改mutable(可变的)
class Person
{
private:
    int _a;

public:
    int b;
    void print() const  // 函数体内部的成员不可以进行修改
    {
        b = 10;  // 报错
    }
};
class Person
{
private:
    int _a;

public:
    mutable int b; // 可变的,在常函数中,这个值可以进行修改
    // 在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
    void print() const // 函数体内部的成员不可以进行修改
    {
        b = 10; // 有mutable修饰,不会报错
    }
};

常对象:

  1. 声明对象前加const称该对象为常对象
  2. 常对象只能调用常函数
const Person p; // 在对象前加const,变为常对象

友元

在程序里,有些私有属性,也项让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。
友元的目的就是让一个函数或者类,访问另一个类中私有成员。
友元的三种实现:

  1. 全局函数做友元
  2. 类做友元
  3. 成员函数做友元
    示例1:全局函数做友元
#include <iostream>
using namespace std;

class Person
{
private:
    int _a;
    int _b;

public:
    Person(int a, int b)
    {
        _a = a;
        _b = b;
    }
    friend void func(Person *pt); // 将全局函数声明为友元函数,这样就可以访问私有成员了,也可以访问公有成员
    void func2() {}
};

// 全局函数
void func(Person *pt)
{
    pt->_a; // 访问私有成员
    pt->_b;
    pt->func2(); // 访问公有成员
}

int main()
{
    Person p1(1,2);
    func(&p1); // 调用友元函数
    return 0;
}

2023.5.26

C++运算符重载

运算符重载概念:对已有的运算符重载进行定义,赋予其另一种功能,以适应不同的数据类型。

  • 运算符重载本质上是定义一个函数,operator是关键字,专门用于定义重载运算符的函数
返回值类型  operator 运算符名称  (形参列表)
{}

加号运算符重载

作用:实现两个自定义数据类型相加的运算
对于内置的数据类型,编译器知道如何进行运算。

  • 示例1:通过成员函数重载+号
#include <iostream>
using namespace std;

class Person
{
public:
    int a;
    int b;
    // 成员函数重载
    Person operator+(Person &p)
    {
        Person temp;
        temp.a = this->a + p.a;
        temp.b = this->b + p.b;
        return temp;
    }
};

void test1()
{
    Person p1;
    p1.a = 1;
    p1.b = 2;

    Person p2;
    p2.a = 3;
    p2.b = 4;
    Person p3 = p1 + p2; // 通过成员函数进行重载

    cout << p3.a << endl; // 输出4
}

int main()
{
    test1();
    return 0;
}
  • 示例2:通过全局函数重载+号
#include <iostream>
using namespace std;

class Person
{
public:
    int a;
    int b;
};

// 全局函数重载+号
Person operator+(Person &p1, Person &p2)
{
    Person temp;
    temp.a = p1.a + p2.a;
    temp.b = p2.b + p2.b;
    return temp;
}

void test1()
{
    Person p1;
    p1.a = 1;
    p1.b = 2;

    Person p2;
    p2.a = 3;
    p2.b = 4;
    Person p3 = p1 + p2; // 通过成员函数进行重载

    cout << p3.a << endl; // 输出4
}

int main()
{
    test1();
    return 0;
}

总结:

  1. 对于内置的数据类型的表达式运算符是不可改变的
  2. 不要滥用运算符重载

继承

继承是面向对象三大特性之一,可用采用继承提高代码的复用率,减少重复的代码。
语法:class 子类:继承方式 父类
子类:也称为派生类
父类:也称为基类

继承方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4vYHLc9-1685094578432)(images/1.png)]
继承一共有三种方式:父类中的private成员,不管使用那种继承方式,都不可以被子类继承

  • 公共继承:继承父类中的public成员于子类中的public,继承父类中的protected成员于子类中的protected
  • 保护继承:继承父类中的publicprotected,在子类中为protected成员
  • 私有继承:继承父类中的publicprotected,在子类中为private成员
    子类的构造函数与析构函数:
  1. 在定义子类时,对继承过来的成员变量的初始化工作,由子类的构造函数完成。
  2. 构造函数:先执行父类的构造函数,再执行子类的构造函数
  3. 析构函数:先执行子类的析构函数,再执行父类的析构函数

继承中的对象模型

在计算子类对象所占内存空间大小时,子类备份了父类所有非静态成员的数据成员

  • 总结:父类中所有非静态成员都会被子类继承下去。父类中私有成员是被编译器给隐藏了,因此是访问不到的,但是确实被继承下去了
#include <iostream>
using namespace std;

class Base
{
public:
    int a;

protected:
    int b;

private:
    int c;
};

class Son : public Base
{
    int d;
};

int main()
{
    cout << sizeof(Base) << endl; // 输出12
    cout << sizeof(Son) << endl;  // 输出16,父类中所有的数据成员都被继承下来,备份了一份
    return 0;
}

子类构造函数与析构函数:

#include <iostream>
using namespace std;

class Base
{
public:
    int a;
    Base()
    {
        cout << "Base的构造函数" << endl;
    }
    ~Base()
    {
        cout << "Base的析构函数" << endl;
    }

protected:
    int b;

private:
    int c;
};

class Son : public Base
{
    int d;

public:
    Son()
    {
        cout << "son构造函数" << endl;
    }
    ~Son()
    {
        cout << "son析构函数" << endl;
    }
};

int main()
{
    Son s1; // 创建子类对象
    return 0;
}

继承中同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据?

  • 访问子类同名成员:直接访问即可
  • 访问父类同名成员:需要加作用域
#include <iostream>
using namespace std;

class Base
{
public:
    int a = 1;
    void func()
    {
        cout << "Base--func()" << endl;
    }
};

class Son : public Base
{
public:
    int a = 2;
    void func()
    {
        cout << "Son--func()" << endl;
    }
};

int main()
{
    Son s1;
    cout << s1.a << endl;       // 输出子类的成员
    cout << s1.Base::a << endl; // 添加父类的作用域,输出父类中的同名成员

    // 访问同名函数
    s1.func(); // 调用的为子类成员函数
    s1.Base::func(); // 调用父类的成员函数
    return 0;
}

继承中同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致。

  • 访问子类同名成员:直接访问即可
  • 访问父类同名成员:需要加作用域

多继承语法

C++允许一个类继承多个类。
语法:class 子类:继承方式1 父类1,继承方式2 父类2,...

  • 多继承可能会引发父类中有同名成员出现,需要加作用域区分
  • C++实际开发中不建议用多继承
#include <iostream>
using namespace std;

class Base1
{
public:
    int a = 1;
};
class Base2
{
public:
    int b = 2;
};

// 多继承
class Son : public Base1, public Base2
{
public:
};

int main()
{
    Son s1;
    cout << s1.a << endl;
    cout << s1.b << endl;
    return 0;
}

菱形继承

菱形继承(钻石继承)的概念:

  • 两个子类继承同一个父类
  • 又有某个类同时继承这两个子类
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TforURkg-1685094578433)(images/2.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoL0fSCo-1685094578435)(images/3.png)]
    菱形继承的问题:
  1. 羊继承了动物的数据,骆驼同样继承了动物的数据,当羊驼继承两者的数据时,会产生二义性
  2. 羊驼继承了两次动物的数据,实际上,我们只需要一次即可。
    当菱形继承时,两个父类拥有相同数据,需要加以作用域区分。这份数据需要一份就可以了,菱形继承导致数据有两份,造成资源浪费。
  • 解决方法:利用虚继承,解决菱形继承的问题。在继承之前,加上关键字virtual变为虚继承
  • 虚继承之后,继承的数据只有一份
#include <iostream>
using namespace std;

// 动物类
class Animal
{
public:
    int age;
};

// 采用虚继承,解决菱形继承的问题。在继承方式前面加上virtual关键字变为虚继承
// Animal类称为虚基类

// 羊类
class Sheep : virtual public Animal
{
};
// 骆驼
class Camel : virtual public Animal
{
};

// 羊驼多继承
class Alpaca : public Sheep, public Camel
{
};

int main()
{
    Alpaca alpaca;

    // 当菱形继承时,两个父类拥有相同数据,需要加以作用域区分
    // 这份数据需要一份就可以了,菱形继承导致数据有两份,造成资源浪费
    alpaca.Sheep::age = 18;            // 数据有二义性,需要指定作用域
    
    cout << alpaca.Sheep::age << endl; // 不采用虚继承时,采用这种方式输出

    cout << alpaca.age << endl; // 采用虚继承后,采用这种方式即可正常输出

    return 0;
}

多态

相关文章:

  • C++学习笔记--黑马程序员
  • 调用华为API实现图像搜索
  • Docker安装MySQL docker安装mysql 完整详细教程
  • mybatis-plus的IPage分页,使用Feign调用,提示无法序列化
  • 【王道·操作系统】第二章 进程管理【未完】
  • 工业相机掉线、丢包、丢帧原因排查
  • springcloud-alibaba (04)GatewayFilter 自定义全局过滤器-认证和授权
  • Windows下搭建paddlenlp 语义检索系统
  • 人生苦短,我用Python
  • 基于langChain 的privateGPT 文档问答 研究
  • 【C++/嵌入式笔试面试八股】一、24.智能指针 | 其他
  • prometheus 部署安装
  • C# 队列(Queue)
  • C语言中的 #ifdef __cplusplus 和 #endif 的作用
  • RK3588-EDGE Ethernet驱动(一)
  • 手持式网络性能测试仪应用于哪些领域及可以完成什么工作?
  • 分享Python采集99个焦点图,总有一款适合您
  • 基于docker容器化的jenkins2.406升级迁移(jdk8升级jdk11)
  • USB xHCI控制器使用总结
  • 如何在 Linux、Windows 和 Mac 上查找 WiFi 密码?
  • Benewake(北醒) 快速实现 TF02-Pro-IIC 与电脑通信操作说明
  • Eclipse教程 Ⅴ
  • 【MySQL】主从复制(两台服务器)
  • 数据库之主键、联合主键
  • openpose原理及安装教程(姿态识别)
  • C++ RapidJSON使用详解
  • JVM-基础知识
  • 【ZYNQ】QSPI Flash 固化程序全攻略
  • Ansys Zemax | 如何模拟部分反射和散射的表面
  • Spring:Spring框架结构 ②