QT_C++
QT
QT是一个跨平台的应用程序和用户界面框架,用于开发图形用户界面(GUI)应用程序以及命令行工具。https://www.qt.io/zh-cn/
部署空项目流程
macos系统
1)启动qt creator,并创建新项目
在此处选择CMake
此处无法选择构建时,在qt偏好设置中找qt版本
添加自己安装的qt的目录,在该目录下找/opt/homebrew/Cellar/qt/6.8.2/bin/qmake
再在构建套件中添加新的构建,例如qt-6.8.2, 选择QT版本为自己安装的QT版本。
完成后可以选择创建项目中的构建了,然后生成如下目录,关掉qt creator,并用vscode打开项目
2) build:这是项目的构建目录,所有构建输出文件都会存放在这里。
- .cmake:CMake 生成的内部配置文件目录,包含一些缓存和临时文件。
- CMakeFiles:CMake 生成的辅助文件目录,包含构建过程中使用的中间文件和配置文件。
- project1_autogen:自动生成的文件目录,通常包含由 CMake 自动生成的代码文件。
project1.app:macOS 应用程序包目录,包含构建生成的 macOS 应用程序。
- Contents:应用程序包的内容目录,包含应用程序的所有资源和可执行文件。
MacOS:应用程序的可执行文件目录,包含实际的可执行文件。
- project1: 可执行文件
- Info.plist:应用程序的配置信息文件,包含应用程序的元数据。
- cmake_install.cmake:CMake 生成的安装脚本文件,用于安装构建生成的文件。
- CMakeCache.txt:CMake 缓存文件,包含项目配置的缓存信息。
- compile_commands.json:CMake 生成的编译命令文件,包含编译每个源文件时使用的编译命令,供 C/C++ 扩展使用以提供代码提示。
- Makefile:CMake 生成的 Makefile 文件,用于使用 make 工具进行构建。
3)vscode配置文件
.vscode目录下
1 | //c_cpp_properties.json |
1 | //launch.json |
1 | //settings.json |
1 | //tasks.json |
4)cmake:
- 使用方法
https://blog.csdn.net/weixin_30908103/article/details/96064514
https://blog.csdn.net/qq_42190402/article/details/142754530
https://zhuanlan.zhihu.com/p/696690726
https://zhuanlan.zhihu.com/p/631107787
https://zhuanlan.zhihu.com/p/461896034?utm_id=0
https://blog.csdn.net/qq_51355375/article/details/142434829
https://blog.csdn.net/qq_51470638/article/details/129101424
https://blog.csdn.net/qq_39698985/article/details/115433257 样例项目目录
上边的工程目录对应的CMakeList.txt1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90cmake_minimum_required(VERSION 3.16)
project(project1 VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
# set(HEADER_DIR ${CMAKE_SOURCE_DIR}/include)
# set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
# set(FORM_DIR ${CMAKE_SOURCE_DIR}/form)
aux_source_directory(src SRC)
FILE(GLOB INC "include/*.h")
list(APPEND CMAKE_AUTOUIC_SEARCH_PATHS "form")
# qt6_wrap_ui(MOC_ ${FORM_DIR}/mainwindow.ui)
# qt6_wrap_ui(UI_HEADERS ${FORM_DIR}/mainwindow.ui)
# include_directories(
# ${CMAKE_SOURCE_DIR}/include
# ${UI_HEADERS}
# )
set(PROJECT_SOURCES
# ${SOURCE_DIR}/main.cpp
# ${SOURCE_DIR}/mainwindow.cpp
# ${UI_HEADERS}
# ${HEADER_DIR}
${SRC}
${INC}
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(project1
MANUAL_FINALIZATION
${PROJECT_SOURCES}
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET project1 APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
add_library(project1 SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(project1
${PROJECT_SOURCES}
)
endif()
endif()
target_include_directories(project1 PRIVATE include)
target_link_libraries(project1 PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.project1)
endif()
set_target_properties(project1 PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
include(GNUInstallDirs)
install(TARGETS project1
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(project1)
endif()
Windows系统
C++知识点
命名空间
命名空间为了解决名字冲突。
命名空间定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18namespace Cir {
const double PI = 3.14;
double areaOfCircle(double radius){
return PI*radius*radius;
}
double length;
class student {
String name;
int age;
void behavior(String name);
}
}
class Cir::student::behavior(String name){
std::cout << name << std::endl;
}
using namespace std;
using Cir::PI;
using MyType = int;
内联函数
内联函数直接在每个调用点(使用点)展开,即函数调用替换为函数代码,减少调用开销(调度栈)1
2
3
4
5
6
7
8
9inline int max(int x, int y){
return x>y?x:y;
}
int main(){
int result = add(5,3);//编译器在此处将add(5,3)替换为代码{return 5>3?5:3;}
std::cout << "Result: " << result << std::endl;
return 0;
}
匿名函数 lambda表达式
匿名函数可以在需要调用的地方直接定义函数,无需单独定义1
2
3
4
5
6
7
8
9
10[cature clause](parameters) -> return_type {
//函数体
//可以使用在捕获列表中的变量
return expression;
}
auto add = [](int a, int b){
return a+b;
}
int sum = add(10,20);
- 捕获列表:获取外部的变量
数组(静态)
- 声明:int myArray[10];
- 初始化: int myArray[5] = {5,4,5,4,2};
- 访问: int value = myArray[2]
引用
引用是变量别名或标签,只能用在已有的变量上。
引用和指针的区别
- 不存在空引用,引用必须连接到一个合法的内存上
- 引用只能在初始化时连接到对象,一旦引用被连接到一个对象上,就不能连接到其他对象上,指针是可以随时指向另一个对象的。
- 引用和指针不能跨作用域使用,只能在对象的生命周期内(为销毁之前)使用。
1 |
|
结构体/类
结构体1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Car{
char* color;
int year;
void (*printCarInfo)();
};
struct Car car;
car.color = "white";
car.printCarInfo = []()->void{printf("this is a Car\n")};
car.pirntCarInfo();
struct Car *AodiA6;
AodiA6 = (struct Car*)malloc(size(struct Car));
AodiA6->color = "black";
AodiA6->printCarInfo = []()->void{printf("this is a AodiA6\n");
AodiA6->printCarInfo();
类包含
数据成员
成员函数
构造函数和析构函数
访问修饰符:public, private, protect
继承关系
生成对象: new
1 | class Car{ |
- 拥有关系:
C++中一个类包含另一个类的对象称为组合,这是一种常见的设计模式,表示一个类是由另一个类的对象组成,这种关系表示拥有关系(has-a)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using namespace std;
class Wheel{
public:
string brand;
string year;
void wheelPrintInfo();
};
void Wheel::wheelPrintInfo(){
cout << brand << endl;
}
class Car{
public
string color;
string brand;
string type;
string year;
Wheel wl;
wheel *wlp;
void realPrintInfo();
};
void Car realPrintInfo(){
cout << type << endl;
}
int main(){
Car *Aodi = new Car();
Aodi->color = "black";
Aodi->wl = Wheel();
Aodi->wl.color = "black";
Aodi->wlp = new Wheel()
Aodi->wlp->brand = "miqi";
Car BMW = Car();
BMW.color = "white";
BMW.wl = Wheel();
BMW.wl.color = "black";
BMW.wlp = new Wheel();
BMW.wlp->brand = "beinaili";
Aodi.realPrintInfo();
BMW.realPrintInfo();
}重载
函数重载
同一个作用域内可以声明几个同名函数,但是函数的形参(参数个数,参数类型和参数顺序)必须不同,对函数的返回类型没有要求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using namespace std;
class printData{
public:
void print(int i){
cout << "整数: "<< i << endl;
}
void print(double f){
cout << "浮点:"<< f << endl;
}
void print(char c[]){
cout << "字符数组:"<< c <<endl;
}
};
int main(){
printData pd;
pd.print(5);
pd.print(500.263);
char c[] = "hello c++";
pd.print(c);
return 0;
}运算符重载
自定义各种运算符在自定义类上的行为特性。
基本原则: - 不可以创建新的运算符
- 至少有一个操作数是用户定义的类型,不能重载两个操作数是两个基本类型的运算符
- 不能更改运算符的优先级和结合性
1 |
|
构造函数
类的构造函数是特殊成员函数,每次创建类的对象时执行,用于构造成员变量的初始化值,分配内存空间等,构造函数没有返回值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;
class Car{
public:
string brand;
int year;
Car(){
brand = "未知";
year = 0;
cout << "无参构造" << endl;
}
void display(){
cout << "brand:" << brand << ", year: " << year << endl;
}
};
int main(){
Car car = Car();
car.display();
return 0;
}
初始化列表
直接在对象的构造过程中初始化成员变量,而不是先创建成员变量后再赋值。1
2
3
4
5
6
7
8
9
10class MyClass{
private:
int a;
double b;
std::string c;
public:
MyClass(int x, double y, const std::string& z): a(x), b(y), c(z){
std::cout << "a: " << a << "b: " << b <<"c:" << c <<endl;
}
};
成员变量初始化顺序是按照他们声明的顺序,而不是初始化列表中的顺序赋值。
this指针
this 指针指向调用对象的指针,在类的作用域中使用,可以明确指出类的成员变量和成员函数。
链式调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
class Car{
public:
string brand;
int year;
Car(){
brand = "未知";
year = 0;
cout << "无参构造" << endl;
}
void display(){
cout << "brand:" << brand << ", year: " << year << endl;
}
Car& setYear(int year){
this->year = year;
return *this;
}
};
int main(){
Car car = Car();
car.display();
//链式调用,car对象调用setYear返回自身,因此可以直接调用对象的display方法
car.setYear(2023).display()
return 0;
}
new和delete关键字
new用于动态分配内存。
- 分配单个对象
int ptr = new int; // int p=(int)malloc(sizeof(int));
MyClass obj = new MyClass(arg1, arg2); - 分配对象数组
int arr = new int[10]; // int a=(int)malloc(sizeof(int)10);
int* arr = new int[10]{1,2,3,4,5}
new要与delete配对使用, new分配的内存必须用delete释放, new[]分配的内存必须delete[]释放 - 释放单个对象
delete ptr; - 释放数组
delete[] arr;拷贝构造函数
拷贝构造函数用于创建一个新对象作为现有对象的副本。被调用的情况:
- 当一个新对象被创建为另一个同类型的现有对象的副本时:
MyClass obj1 = obj2; 或者MyClass obj1(obj2); 其中obj2是现有对象。 - 将对象作为参数传递给函数时(按值传递):
当对象作为参数传递给函数,并且参数不是引用时,会使用拷贝构造函数创建函数内部的对象副本。 - 从函数返回对象时(按值返回):
最为函数返回值,并且没有使用引用或指针时,拷贝构造函数用于从函数返回值创建副本。 - 初始化数组或容器中的元素时:深拷贝和浅拷贝是处理对象拷贝时的两种方法,尤其是在对象包含指针或者动态分配内存时。
1
2
3
4class MyClass{
public:
MyClass(const MyClass& other){};//other是对同类型对象的常量引用,即用的对象是常量,不可修改。
};浅拷贝
浅拷贝只复制对象的成员变量的值。如果成员变量是指针,则复制指针的值(即内存地址),而不是指针所指向的实际数据。这会导致多个对象共享相同的内存地址。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using namespace std;
class Shallow{
public:
int* data;
Shallow(int d){
data = new int(d); //动态分配
cout << "观察数据:"<<endl;
cout << d << endl;
cout << *data << endl;
cout << "观察内存在构造函数中:" << endl;
cout << data <<endl;
}
//默认的拷贝构造函数是浅拷贝,可以不用显示定义
Shallow(const Shallow& obj){
data = obj.data //复制地址而不是数据
}
~Shallow(){
delete data;
}
};
int main(){
Shallow obj1(20);
Shallow obj2 = obj1;
cout << "观察内存在main函数obj2的data地址:"<<endl;
cout << obj2.data << endl;
cout <<"*obj1.data:"<<*obj1.data<<", *obj2.data:" << *obj2.data << endl;
return 0;
}深拷贝
深拷贝复制对象成员变量的值以及指针所指向的实际数据。这意味着创建新的独立副本,避免了共享内存地址的问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using namespace std;
class Deep{
public:
int* data;
Shallow(int d){
data = new int(d); //动态分配
cout << "观察数据:"<<endl;
cout << d << endl;
cout << *data << endl;
cout << "观察内存在构造函数中:" << endl;
cout << data <<endl;
}
//显示定义深拷贝的构造函数
Deep(const Deep& source){
data = new int(*source.data);//复制数据,而不是地址
cout << "深拷贝构造函数"<<endl;
}
~Deep(){
delete data;
}
};
int main(){
Deep obj1(20);
Deep obj2 = obj1;
cout << "观察内存在main函数obj2的data地址:"<<endl;
cout << obj2.data << endl;
cout <<"*obj1.data:"<<*obj1.data<<", *obj2.data:" << *obj2.data << endl;
return 0;
}规则三则
规则三则(rule of three)是一个面向对象编程原则,涉及到类的拷贝控制。规则三则指出,如果你需要显式地定义或重载类的任何一个拷贝控制操作(拷贝构造函数、拷贝赋值运算符、析构函数),那么你几乎肯定需要显示地定义或重载所有三个。这是因为这三个功能通常都是用于管理动态分配的资源,比如在堆上分配的内存。确保了动态分配资源正确管理,避免了内存泄漏和浅拷贝问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyClass{
private:
char* buffer;
public:
//构造函数
MyClass(const char* str){
if (str){
buffer = new char[strlen(str)+1];
strcpy(buffer,str);
}else{
buffer = nullptr;
}
}
//析构函数
~MyClass(){
delete[] buffer;
}
//拷贝构造函数
MyClass(const MyClass& other){
if (other.buffer){
buffer = new char[strlen(other.buffer)+1];
strcpy(buffer, other.buffer);
}else{
buffer = nullptr;
}
}
//赋值运算
MyClass& operator=(const MyClass& other){
//自赋值检查
if (this == &other) return *this;
//释放原有空间
delete[] buffer;
buffer = new char[strlen(other.buffer)+1];
strcpy(buffer, other.buffer);
return *this;
}
}避免不必要的拷贝
避免不必要的拷贝是C++设计的重要原则,尤其是在处理大型对象或资源密集型对象时。使用引用(包括常量引用)和移动语义是实现这个目标的两种常见方法。 - 使用引用传递对象
通过使用引用来传递对象,可以避免在函数调用时创建对象的副本。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std;
class LargeObject{
//假设这是一个占用大量内存的大型对象
};
void processLargeObject(const LargeObject& obj){
//处理对象,但不修改它
cout << "Processing object..."<<endl;
}
int main(){
LargeObject myLargeObject;
processingLargeObject(myLargeObject);//通过引用传递,避免拷贝
return 0;
} - 使用移动语义
移动语义允许资源(动态分配内存)的所有权从一个对象转移到另一个对象,这避免了不必要的拷贝。
左值 :表达式结束后依然存在的持久对象,可取地址。
右值 :表达式结束后就不再存在的临时对象,不可取地址。
例如:int x1 = 1; \1是右值,x1是左值
为支持移动语义引入新的引用类型,称为”右值引用”,使用”&&”来声明。而使用”&”声明的引用,则称为”左值引用”。
右值引用只能够引用没有名称的临时对象以及使用std::move标记的对象。由于右值都是临时的值,临时值释放后也就不再持有属性的所有权。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using namespace std;
class MovableObject{
private:
char* buffer;
public:
MovableObject(){
//构造函数
}
MovableObject(const MovableObject& other){
//拷贝构造函数(费资源)
}
MovableOject& operator=(const MovableObject& other){
//拷贝赋值费资源
return *this;
}
MovableObject(Movable&& other) noexcept{
//移动构造函数,移动资所有权,省资源
buffer = other.buffer;
other.buffer = nullptr;//临时变量,右值释放资源
}
MovableOject& operator=(MovableObject&& other){
//移动赋值省资源
if (this == &other) return *this;
delete[] buffer;
buffer = other.buffer;
other.buffer = nullptr; //临时变量,右值释放资源
return *this;
}
~MovableObject(){
delete[] buffer;
}
};禁用拷贝构造函数
- delete关键字明确指定不允许拷贝构造:
1
2
3
4
5
6
7
8
9
10
11
12
13class NonCopyable{
public:
NonCopyable() = default; //使用默认构造函数
//禁用拷贝构造函数
NonCopyable(const NonCopyable& ) = delete;
//禁用拷贝赋值运算符
NonCopyable& operator=(const NonCopyalbe&) = delete;
};
int main(){
NonCopyable obj1;
//NonCopyable obj2 = obj1; //编译报错,拷贝构造函数被禁用。
return 0;
} - private声明拷贝构造和拷贝赋值+不提供它们具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class NonCopyable{
public:
NonCopyable() = default; //使用默认构造函数
private:
//禁用拷贝构造函数
NonCopyable(const NonCopyable& );
//禁用拷贝赋值运算符
NonCopyable& operator=(const NonCopyalbe&);
};
int main(){
NonCopyable obj1;
//NonCopyable obj2 = obj1; //编译报错,无法访问私有的拷贝构造函数。
return 0;
}
要点 | 描述 |
---|---|
定义和作用 | 拷贝构造函数在创建对象作为另一个现有对象副本时调用,通常有一个对同类型对象的常量引用参数。 |
语法 | 典型声明ClassName(const ClassName& other) |
深拷贝和浅拷贝 | 浅拷贝复制值,深拷贝创建资源的独立副本。对于包含指针的类,深拷贝通常必要。 |
规则三则 | 如果实现拷贝构造函数、拷贝赋值运算符或析构函数中的任何一个,通常应该实现所有三个。 |
避免不必要的拷贝 | 对大型对象,使用移动语义避免不必要的拷贝,并在传递对象时使用引用或者指针。 |
拷贝构造函数的隐式调用 | 不仅在显式复制时调用,也可能在将对象作为函数参数传递、从函数返回对象时隐式调用。 |
禁用拷贝构造函数 | 对某些类,可以通过将拷贝构造函数声明为私有或使用delete关键字禁用拷贝。 |
析构函数
析构函数在对象生命周期结束时被自动调用,用于执行对象销毁前的清理工作。析构函数对于涉及动态分配资源(内存、文件句柄、网络连接等)情况特别重要。
- 语法:~MyClass()
- 无返回值和参数
- 自动调用:当对象生命周期结束时(例如,一个局部对象的作用域结束,或者使用delete删除动态分配的对象),析构函数会被自动调用。
- 不可重载:每个类只能有一个析构函数。
- 继承和多态:如果一个类是多态基类,其析构函数应该是虚的。
静态成员
静态成员包括静态成员变量和静态成员函数。特点如下
- 静态成员变量
- 定义:静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建多少个类实例,静态成员变量只有一份拷贝。
- 初始化:静态成员变量需要在类外进行初始化,通常在类的实现文件中。
- 访问:静态成员变量可以通过类型直接访问,不要创建类的对象。也可以通过类的对象访问。
- 用途:常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共享。
- 静态成员函数
- 定义:静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和非静态成员函数。
- 访问:可以通过类名直接调用,也可以通过类的实例调用。
- 用途:通常用于实现与具体对象无关的功能,或访问静态成员变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class MyClass{
public:
static int staticValue;//静态变量
MyClass(){
staticValue++;
}
~MyClass(){
staticValue--;
}
static int getStaticValue(){
return staticValue;
}
};
//类外初始化静态成员变量
int MyClass::staticValue = 0;
int main(){
MyClass obj1, obj2;
std::cout << MyClass::staticValue << std::endl;
std::cout << MyClass::getStaticValue() << std::endl;
std::cout << obj1.getStaticValue() << std::endl;
}继承
继承允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码的重用,以及建立一种类型之间的层次关系。其特点如下:
- 代码重用:子类继承了父类的属性和方法,减少代码的重复编写。
- 扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
- 多态性:通过继承和虚函数,允许在运行时决定调用哪个函数(某个子类的函数或者父类的函数)
- 继承访问权限:public,protect,private
不同访问级别的继承如何影响基类的不同访问级别的成员在派生类中的访问级别
|基类成员类型|public继承|protected继承|private继承|
|—-|—-|—-|—-|
|public|public|protected|private|
|protected|protected|protected|private|
|private|不可访问|不可访问|不可访问|
1 | class Base{//基类 |
分文件方式实现继承
1 | //animal.h |