【C++】基础1——开发环境和基础概念

1. 一些概念

1.1 面向对象编程(OOP)特性

封装:将数据和对数据的访问封装在类中,数据作为成员,访问方法作为成员函数。

继承:子类继承父类而重写父类特性,统计也继承父类特性。

抽象:抽象基类作为接口。

多态:基类指针运行时初始化为某子类对象,运行时就使用该子类对象的方法。

1.2 构造函数与析构函数

1.2.1 构造函数

一种特殊的成员函数,其名称与类名相同,无返回类型,可重载(一个类可以有多个构造函数)。在创建类的对象时会被自动调用,主要用于对对象的数据成员进行初始化。构造函数分类:默认构造函数,带参数构造函数,复制构造函数。

默认构造函数

默认构造函数有三种:没有参数的构造函数;所有参数都有默认值的构造函数;编译器自动生成的默认构造函数(类没有定义构造函数时)。

class A
public:
    int a = 0;
    int b = 0;
    // 默认构造函数1
    A(){
        std::cout << "default construction function(no param)" << std::endl;
    }
    // 默认构造函数2
    A(int a=0, int b=0){
        this->a = a;
        this->b = b;
        std::cout << "default construction function(default param)" << std::endl;
    }
};

class B{};

// 创建对象
// A a;  		// error,无参数创建对象,有两个备选的构造函数
A a(1); 		// 使用默认构造函数2
A a(1, 2);  	// 使用默认构造函数2

B b;  			// 使用编译器自动生成的默认构造函数
B *bb = new B();// 使用new创建对象	
带参数的构造函数

一个类可以有多个带参数的构造函数,但是需要保证参数个数不同,以避免构造函数调用歧义。

class A{
public:
    int a = 0;
    int b = 0;
    A(int a, int b):a(a), b(b){}
    A(int a):a(a){}
}

// 创建对象
A a(1, 2);
A a(1);
A *a = new A(1);
复制构函数
  • 默认的和自定义的复制构造函数

用于使用对象创建一个副本对象。如果没有显式定义,编译器会自动生成一个默认的拷贝构造函数,它会逐个复制对象的数据成员。编译器默认的拷贝构造函数采用浅拷贝方法。注意:如果只定义拷贝构造函数,则拷贝构造函数会负载编译器自动生成的默认构造函数,此时还应该显式创建默认构造函数。

class A{
public:
    int a = 0;
    int b = 0;
    A(){}
    A(const A& copy){
        this->a = copy.a;
        this->b = copy.b;
    }
};

// 创建对象
A a1;	   // 使用显式定义的默认构造函数创建对象a1
A a2(a1);  // 使用拷贝构造函数创建a1的副本对象a2

class B{
public:
    int a = 0;
    int b = 0;
};

// 创建对象
B b1;     // 使用编译器自动生成的默认构造函数创建对象b1
B b2(b1); // 使用编译器自动生成的复制构造函数创建对象b1的副本对象b2

  • 深拷贝和浅拷贝

浅拷贝:复制对象时,只拷贝数据成员的值(基础类型变量拷贝值,指针变量拷贝指针值),可能导致不同对象的指针类型成员变量指向同一内存区域。如果某个对象销毁,则其他对象的指针成员将称为野指针(指向非法 内存地址)。编译器自动生成的复制构造函数采用浅拷贝方式。
深拷贝:复制对象时,对于基础类型成员变量,拷贝其值,对于指针成员,开辟新的内存区域,然后复制源对象指针成员指向的内存区域。

#include <iostream>
#include <cassert>

class A{
public:
    int a = 0;
    int b = 0;
    int *arr = nullptr;
    int size = 0;
    A(){}
    // deep copy
    A(const A &copy){  
        this->a = copy.a;
        this->b = copy.b;
        if(  copy.arr != nullptr){
            this->arr = new int[copy.size];
            for(int i = 0; i < copy.size; i++){
                this->arr[i] = copy.arr[i];
            }
        }
    }
};

class B{
public:
    int a = 0;
    int b = 0;
    int *c = nullptr;
};

int main(){
    A a;
    a.a = 1;
    a.b = 2;

    a.size = 3;
    a.arr = new int[a.size];
    for(int i = 0; i < a.size; i++){
        a.arr[i] = i;
    }

    A aCopy1(a);   					// 调用自定义的拷贝构造函数,深拷贝
    A aCopy2 = a;  					// 调用自定义的拷贝构造函数,深拷贝
    assert(a.arr == aCopy1.arr);  	// 深拷贝,对象a和其副本对象aCopy1各自的arr指针成员指向不同的内存区域,断言失败
    assert(a.arr == aCopy2.arr);	// 深拷贝,对象a和其副本对象aCopy1各自的arr指针成员指向不同的内存区域,断言失败
    
    B b;
    B bCopy1(b);   // 调用默认拷贝构造函数
    B bCopy2(b);   // 调用默认拷贝构造函数
    assert(a.arr == aCopy1.arr);	// 浅拷贝,对象b和其副本对象bCopy1各自的arr指针成员指向相同的内存区域,断言成功
    assert(a.arr == aCopy2.arr);	// 浅拷贝,对象b和其副本对象bCopy1各自的arr指针成员指向相同的内存区域,断言成功
    return 0;
}

1.2.2 析构函数

一种特殊的成员函数,其名称为类名加上 “~” 前缀,无返回类型,不可重载(一个类只能有一个析构函数)。在对象的生命周期结束时被自动调用,主要用于释放对象所占用的资源,如动态分配的内存、打开的文件、网络连接等。

#include <iostream>
class A{
public:
    int arrSize = 0;
    int *arr = nullptr;
    ~A(){
        if(this->b != nullptr){
            delete this->b;
        }
    }
};

int main(){
    A a;
    a.arrSize = 10;
    a.arr = new int(a.arrSize);
    for(int i=0; i<a.arrSize; i++){
        a.arr[i] = i;
    }
    return 0;
}

调用顺序:创建一个派生类对象时,先调用基类构造函数,再调用派生类的构造函数;销毁一个对象时,先调用派生类的析构函数,再调用基类的析构函数。

#include <iostream>

class A{
public:
    A(){
        std::cout << "calling A()" << std::endl;
    }
    ~A(){
        std::cout << "calling ~A()" << std::endl;
    }
};
class B: public A{
public:
    B(){
        std::cout << "calling B()" << std::endl;
    }
    ~B(){
        std::cout << "calling ~B()" << std::endl;
    }
};

int main() {
    B *b = new B();
    delete b;
    return 0;
}

1.2.3 补充

  • 调用拷贝构造函数的时机:创建副本对象,传参,返回值。
  • 如果函数接收基类类型参数,传如派生类对象时,则将调用基类的拷贝构造函数。拷贝派生类对象中属于基类的成员,去掉派生类对象中自己特有的成员,称为对象切片。
  • 只能使用delete销毁使用new创建的对象。B b; delete &b;非法。

1.3 单例模式

构造函数不一定要是private的,可以通过private构造函数来实现单例模式。

#include <iostream>

class Singleton {
private:
    // private 构造函数
    Singleton() {}
    static Singleton* instance;
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

int main() {
    Singleton* s = Singleton::getInstance();
    return 0;
}

2. vscode C++ 编程环境快速搭建

2.1 编译工具链

2.1.1 Windows:MinGW64

MinGW64

地址:https://www.mingw-w64.org/downloads/

说明:注意下载预编译好的工具链而不是源码文件,选w64devkit或者MinGW-W64-builds都行。进入github从assets中选择压缩包下载,然后直接解压,把二进制文件目录配置到系统环境变量。

关于压缩包文件名
在这里插入图片描述

  • I686/x86_64:目标架构为32位/64位。

  • 14.2.0:一般指 GCC(GNU Compiler Collection)编译器的版本号。GCC 是 MinGW 工具链的核心组件,这个版本号表明该工具链使用的 GCC 编译器版本为 14.2.0。

  • release:发布版而非调试版。

  • posix/win32/mcf: 工具链支持操作系统编程接口。

    • POSIX(Portable Operating System Interface)标准,类 UNIX 系统(如 Linux、FreeBSD 等)遵循之。 支持 POSIX 意味着该工具链可以在 Windows 系统上模拟 POSIX 环境,使得原本为类 Unix 系统编写的代码在 Windows 上也能更方便地编译和运行,例如支持 POSIX 线程库等。
    • Win32:Windows 操作系统提供的 32 位编程接口。涵盖了图形用户界面(GUI)、文件系统、网络通信、进程管理等各个方面。
    • mcf:略。
  • Seh/dwarf:工具链使用支持异常处理机制

    • SEH:结构化异常处理(Structured Exception Handling),是 Windows 操作系统提供的一种异常处理机制。主要适用于 Windows 系统,在windows系统上性能更好。
    • dwarf:一种用于在可执行文件中存储调试信息的标准格式(Debugging With Attributed Record Formats),在 GCC 等编译器中,DWARF 被用作一种异常处理机制,主要用于类 Unix 系统和 32 位的 MinGW 环境中。
  • ucrt/msvcrt:工具链使用的C语言运行时库。C语言运行时库包含 C 语言标准库,还包含一些特定于该操作系统或编译器的扩展函数和功能,以提供对底层系统的访问和特定的运行时支持。

    • msvcrt:Microsoft Visual C++ Runtime Library。它是微软提供的一组动态链接库(DLL)和静态库,包含了一系列标准 C 库函数以及部分 C++ 标准库的实现。
    • ucrt: Universal C Runtime Library。它是 Windows 10 引入的新一代 C 运行时库,替代了之前的 MSVCRT(Microsoft Visual C++ Runtime Library)
  • rt_v12:运行时库版本。

  • ver1.7:版本的修订号。

2.1.2 Linux:GCC

使用apt包管理的
  • 下载build-essential

build-essential:一个元包(metapackage),它会依赖并安装多个与软件开发相关的核心包(编译工具:gcc,g++,构建工具:make,dpkg-dev(提供软件包管理的工具和文件),libc6-dev(C标准库文件),其它工具:binutils(as,ld))。它开发者提供一个完整的软件开发环境,使得开发者能够顺利编译、构建和测试各种 C 和 C++ 程序。如果只想快速得到一个C/C++编程环境,只下载build-essential就够了。

sudo apt install build-essential
  • 加入PPA源

PPA(Personal Package Archive)源即个人软件包归档源,是Launchpad.net为 Ubuntu 及基于 Ubuntu 的衍生发行版用户提供的一种软件源扩展机制,用户能够通过 PPA 源轻松获取到在 Ubuntu 官方软件源中不存在,或者版本较新的软件。

sudo add-apt-repository ppa:ubuntu-toolchain-r/test

该命令将添加一个名为ubuntu-toolchain-r/test的ppa源,具体地,命令执行后会在/etc/apt/sources.list.d生成ubuntu-toolchain-r-ubuntu-test-focal.list,内容如下。前缀为deb的地址是软件二进制包地址,前缀为deb-src的为软件源代码地址。

  • 更换PPA源

Launchpad.net提供的PPA源下载速度慢,考虑使用国内源中科大PPA源(launchpad.proxy.ustclug.org)。修改 /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-focal.list 将 ppa.launchpad.net 换成 launchpad.proxy.ustclug.org

修改前

# deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu focal main
deb http://launchpad.proxy.ustcluh.org/ubuntu-toolchain-r/test/ubuntu focal main

# deb-src http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu focal main

修改后

# deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu focal main
deb http://launchpad.proxy.ustcluh.org/ubuntu-toolchain-r/test/ubuntu focal main

# deb-src http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu focal main

使用ppa源更新软件包信息

sudo apt update

补充

sudo apt update执行过程:从源地址下载软件包索引文件(根据时间戳和检验和判断是否需要下载索引文件)到/var/lib/apt/lists/目录,解压索引文件得到软件包详细信息,将其合并到本地软件包数据库(用于软件包搜索,安装,更新)。源地址搜索顺序:/etc/apt/sources.list文件 -> /etc/apt/sources.list.d目录下的各个list文件。

  • 安装指定版本gcc或者g++
sudo apt install gcc-13
sudo apt install g++-13
使用yum包管理的(undone)

2.1.3 MacOS:Clang(undone)

2.2 vscode

下载vscode加速方法:https://zhuanlan.zhihu.com/p/112215618

C/C++ Extension:IntelliSense(代码高亮,悬停展示信息,代码补全)

2.2.1 task.json

配置文件路径:.vscode/tasks.json

{
  "tasks": [
    {
      "type": "cppbuild",
      "label": "C/C++: g++.exe build active file",
      "command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
      "args": [
        "-fdiagnostics-color=always",
        "-g",
        "${file}",
        "-o",
        "${fileDirname}\\${fileBasenameNoExtension}.exe"
      ],
      "options": {
        "cwd": "${fileDirname}"
      },
      "problemMatcher": ["$gcc"],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "detail": "Task generated by Debugger."
    }
  ],
  "version": "2.0.0"
}

Ref:https://code.visualstudio.com/docs/editor/tasks#_custom-tasks

  • type:构建任务类型,可以是cppbuild(将任务与内置的构建功能集成,如vscode快捷键触发任务), shell(command被解释为一个shell命令), process(command被解释为一个要执行的进程)

  • label:用于launch.json中配置某个调试项所使用的task

  • cwd:command执行的当前工作路径

  • problemMathcer:用于指定解析编译器输出(error,warning)解析器,对于GCC,使用$gcc

  • group:指定task所属组,如多个task属于test组,则在vscode控制面输入Run Test Task命令即可运行这些task。常见的group有test,build, none。

2.2.2 Launch.json

配置文件路径:.vscode/launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "gdb",
            "request": "launch",
            "name": "script",
            "program": "${workspaceFolder}/build/${fileBasenameNoExtension}",
            "args": [],
            "cwd": "${workspaceFolder}",
            "preLaunchTask": "oneFile"
        }
    ]
}

ref: https://code.visualstudio.com/docs/editor/debugging-configuration

必选参数

  • type - the type of debugger to use for this launch configuration. Every installed debug extension introduces a type: node for the built-in Node debugger, for example, or php and go for the PHP and Go extensions.
  • request - the request type of this launch configuration. Currently, launch and attach are supported.
  • name - the reader-friendly name to appear in the Debug launch configuration dropdown.

可选参数

  • program - executable or file to run when launching the debugger

  • args - arguments passed to the program to debug

  • env - environment variables (the value null can be used to “undefine” a variable)

  • preLaunchTask - to launch a task before the start of a debug session, set this attribute to the label of a task specified in tasks.json (in the workspace’s .vscode folder).

3. C++基础

3.1 函数创建和使用

// 函数形式1:传值形式的参数

int add(int a, int b);  // declaration
int add(int a, int b){  // definition
   return a + b;
}

int main(){
   int ret = 0;
   int a = 1;
   int b = 2;
  ret = add(a, b);  // call function: ret=3
}

// 函数形式2:传址形式的参数
void swap(int *a, int *b);  // declaration
void swap(int *a, int *b){  // definition
   int tmp = *a;
   *a = *b;
   *b = tmp;
}

int main(){
   int ret = 0;
   int a = 1;
   int b = 2;
   swap(&a, &b);  // call function:a=2,b=1
}

// 函数形式3:缺省参数(缺省参数应该连续地排列在参数列表右侧)  

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

int main(){
   int ret = 0;
   int a = 1;
   int b = 2;
   int c = 3;
  ret = add(a);     // call function:ret=1
  ret = add(a, b);   // call function:ret=3
  ret = add(a, b, c);  // call function:ret=6
}

// 函数形式4:不定参数(略)

3.2 文件的组织

3.2.1 头文件作用

  • 声明函数、类和变量。以供多个源文件使用,避免重复定义和声明
  • 有利于模块化组织,将与一个功能相关的变量,函数,类组织到一个头文件。
  • 便于分离接口和实现。

3.2.2 如何编写头文件

  • 头文件保护符,防止源文件重复引用。
/* 方法一: 标准方法 */
#ifndef UTILS_H
#define UTILS_H
// 头文件内容
#endif

/* 方法二:非标准方法 */
#pragma once
// 头文件内容
  • 最小依赖:头文件可能会依赖其他头文件,要保证依赖的头文件都是必要的。如果头文件只需某个类的声明,不需要完整定义,可以使用前向声明而不是引入头文件的方式
#ifndef UTILS_H
#define UTILS_H

// 前向声明
class OtherClass;

class MyClass {
public:
    void doSomething(OtherClass* other);
};

#endif
  • 声明和实现分离:头文件中最好只包含必要的声明和定义,复杂逻辑应该放到源文件中实现。如,utils.h和utils.cpp一般出现在一个目录中,utils.h为接口,utils.cpp实现utils.h中函数,类。

  • 注意命名规范和必要的注释:文件名采用下划线分割,标识符用驼峰法。

3.3 类的创建和使用

3.3.1 抽象类

  • 至少有一个纯虚函数被称为抽象类,抽象类不能用于创建对象,只能用于继承并重写,且要求一定要重写纯虚函数。
  • 派生类没有实现纯虚函数则派生类变为抽象类。纯虚函数被派生类实现后成为虚函数。
  • 可以继承一个普通类,然后重写出纯虚函数将得到抽象类,这违背从抽象到具体的实践原则,不推荐。
#include <iostream>

class Shape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    void display() const {
        std::cout << "This is a shape." << std::endl;
    }
};

// 派生类 Circle
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
};

// 类对象方式
Circle circle(5.0);
circle.display();
// 多态方式
Shape* shape = &circle;
shape->display();
接口:一种特殊的抽象类
  • 类中没有定义任何的成员变量
  • 所有的成员函数都是公有的
  • 所有的成员函数都是纯虚函数
// 接口Channel
class Channel
{
public:
    virtual bool open() = 0;
    virtual void close() = 0;
    virtual bool send(char* buf, int len) = 0;
    virtual int receive(char* buf, int len) = 0;
};

3.3.2 基类和派生类

虚函数和普通成员函数的异同

相同:都可以被派生类重写

不同:

  • 函数地址绑定方式不同:普通成员函数使用静态绑定,在编译阶段根据类型绑定成员函数地址。例如,对Circle circle(5.0),在编译阶段就可以通过类型Circle确定普通成员函数的调用位置。虚函数通过动态绑定,根据在运行时的类型才能确定调用的成员函数的地址。
  • 虚函数需要使用virtual关键字,使用虚表,有点性能开销。重写虚函数才能加override关键字(不加也行,推荐加),重写普通成员函数不能加override关键字。
  • 多态性:普通成员函数不支持多态(即使用基类指针指向派生类对象,来调用普通成员函数,调用的仍然是基类的成员函数;被重写的普通成员函数只能被显式创建派生类对象去调用),虚函数支持多态。
#include    <iostream>
class Base{
public:
    virtual void func1(){
        std::cout << "func1 from Base Class" << std::endl;
    }
    void func2(){
        std::cout << "normal member function in Base Class" << std::endl;
    }
};

class Derived : public Base{
public:
    void func1() override{
        std::cout << "func1 from Derived Class" << std::endl;
    }
    void func2(){
        std::cout << "normal member function in Derive Class" << std::endl;
    }
};

int main()
{
    Base a;
    a.func1();
    a.func2();
    std::cout << "-------------------" << std::endl;
    Base *b = new Derived();
    b->func1(); 
    b->func2();
    std::cout << "-------------------" << std::endl;
    Derived c;
    c.func1(); 
    c.func2();
    return 0;
}
/* RESULT:
func1 from Base Class
normal member function in Base Class
-------------------
func1 from Derived Class
normal member function in Base Class
-------------------
func1 from Derived Class
normal member function in Derive Class
*/

3.3.3 类数据成员和对象数据成员

类数据成员/静态数据成员:声明时加static关键字,直接使用类名来引用,也可以通过对象来引用(不推荐)。基本数据类型(int,float,char)的静态数据成员不显式初始化,编译器使用零值来初始化。

对象数据成员:每个对象拥有自己的对象数据成员。可以在定义处初始化对象数据成员,然后根据需要在构造函数中再初始化数据成员,避免引用未初始化的成员变量。

class A{
public:
    static int a = 1;
    int b = 0;
}

// 引用类的数据成员
cout << A.a;  // 引用类成员变量/类成员变量
A a;
cout << a.b;  // 引用普通成员变量

/* 初始化普通成员变量的规范:定义处初始化,构造函数处再初始化 */
class B
public:
    static int a = 1;
    int b = 0;
	B(int b):b(b){}  // 构造函数中初始化对象的数据成员,因此不应该声明变量a
};

3.3.4 静态成员函数和非静态成员函数

静态成员函数:static修饰,只能访问静态数据成员和其他静态成员函数。不能通过this指针引用。可以用过类名或者对象来调用静态成员函数。

非静态成员函数:可以访问静态数据成员,非静态数据成员,静态成员函数,非静态成员函数,可以通过this指针引用。仅可以通过对象来调用。

3.3.5 成员的访问权限

权限等级:外界可以访问 > 本类和派生类成员函数可以访问 > 本类成员函数可以访问。

public

外部,本类成员函数(public,private,protected),派生类成员函数都能直接访问public成员。

#include <iostream>
class A{
public:
    int public_data = 0;
    void get_public_data(){  // 内部
        std::cout << "public_data: " << public_data << std::endl;
    }
};


// 访问
A a;
a.public_data = 1;    // 外部通过对象直接访问公有数据成员
a.get_public_data();  // 外部通过对象直接访问公有成员函数
protected

本类成员函数,被派生类成员函数可以直接访问protected成员。外部不能直接访问protected成员,只能通过公共接口(public成员函数)间接访问。

class A{
public:
    void get_protected_data(){
        std::cout << "protected_data" << protected_data << std::endl;
    }
    void set_protected_data(int data){
        protected_data = data;
    }
protected:
    int protected_data = 0;
};

class B: public A{
public:
    void derived_get_protected_data(){
        std::cout << "protected_data" << protected_data << std::endl;
    }
    void derived_set_protected_data(int data){
        protected_data = data;
    }
};

// 访问
A a;
a.set_protected_data(2);
a.get_protected_data();
// a.protected_data = 1;  // error
B b;
b.derived_set_protected_data(3);  // 派生类访问protected成员
b.derived_get_protected_data();
// b.protected_data = 1;  // error
private

私有成员只能被本类成员函数访问。

#include <iostream>
class A{
public:
    void get_private_data(){
        std::cout << "private_data" << private_data << std::endl;
    }
    void set_private_data(int data){
        private_data = data;
    }
private:
    int private_data = 0;
};

// 外部访问
a.get_private_data();  // 外部通过对象的公开接口间接访问私有成员函数
a.set_private_data(2);  // 外部通过对象的公开接口间接访问私有成员函数
// a.private_data = 3;  // error

3.3.6 继承限定规则

public

基类public成员在派生类中仍然是public成员;基类protected成员在派生类中仍是protetced成员;基类private成员在派生类中只能通过基类提供的public或者protected成员函数间接访问。

class A{
public:
    int public_data = 1;
protected:
    int protected_data = 2;
private:
    int private_data = 3;
};

class B: public A{
public:
    void print(){
        std::cout << public_data << std::endl;     // 经过public继承,public_data仍为public的
        std::cout << protected_data << std::endl;  // 经过public继承,protected_data仍为protected的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
protected:
    void protected_print(){
        std::cout << public_data << std::endl;     // 经过public继承,public_data仍为public的
        std::cout << protected_data << std::endl;  // 经过public继承,protected_data仍为protected的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
};

// 访问
B b;
b.public_data = 11;         // 直接访问public成员
// b.protected_data = 22;   // 直接访问protected成员,error
// b.private_data = 33;     // 直接访问父类的private成员,error

b.print();                  // 间接访问
protected

基类public和protected成员在派生类中都成了protected成员,派生类只能通过基类提供的public或者protected成员函数间接访问基类private成员。

class A{
public:
    int public_data = 1;
protected:
    int protected_data = 2;
private:
    int private_data = 3;
};
class B: protected A{
public:
    void print(){
        std::cout << public_data << std::endl;     // 经过protected继承,public_data为protected的
        std::cout << protected_data << std::endl;  // 经过protected继承,protected_data仍为protected的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
protected:
    void protected_print(){
        std::cout << public_data << std::endl;     // 经过protected继承,public_data为protected的
        std::cout << protected_data << std::endl;  // 经过protected继承,protected_data仍为protected的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
};
// 访问
B b;
// b.public_data = 11;      // 直接访问protected成员,error
// b.protected_data = 22;   // 直接访问protected成员,error
// b.private_data = 33;     // 直接访问父类的private成员,error

b.print();                  // 间接访问

private

基类的public成员和protected成员在派生类中都变成private成员,派生类只能通过基类提供的public或者protected成员函数间接访问基类private成员。

class A{
public:
    int public_data = 1;
protected:
    int protected_data = 2;
private:
    int private_data = 3;
};
class B: private A{
public:
    void print(){
        std::cout << public_data << std::endl;     // 经过private继承,public_data变为private的
        std::cout << protected_data << std::endl;  // 经过private继承,protected_data变为private的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
protected:
    void protected_print(){
        std::cout << public_data << std::endl;     // 经过private继承,public_data变为private的
        std::cout << protected_data << std::endl;  // 经过private继承,protected_data变为private的
        // std::cout << private_data << std::endl; // 不可访问基类private成员,error
    }
};
// 访问
B b;
// b.public_data = 11;      // 直接访问private成员,error
// b.protected_data = 22;   // 直接访问private成员,error
// b.private_data = 33;     // 直接访问父类的private成员,error

b.print();                  // 间接访问
总结

总共只有3条规则:派生类不得直接访问基类private成员;public继承使得基类成员(除基类private成员)访问限制在派生类中不变;protected继承使得基类成员(除了private成员)访问限制在派生类中变为protected。

3.3.7 补充

覆写数据成员

派生类定义和基类同名的数据成员,则派生类的定义覆盖父类的定义,并且派生类的派生类都会沿用继承路径上最近的基类对该数据成员的定义。

class A{
public:
    int public_data = 0;
};
class B: public A{
public:
    float public_data = 0.1;
};
class C: public B{
public:
    void print(){
        std::cout << public_data << std::endl;     // 0.1
        std::cout << A::public_data << std::endl;  // 0
    }
};

// 访问
C c;
c.print();
私有成员函数的作用
  • 作为public成员函数的工具函数。
  • 作为构造函数初始化资源的操作,析构函数释放资源的操作。
  • 管理内部状态变量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值