类的基本概念
类的定义
c
语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
cpp
是基于面向对象的,关注的是对象,将一件事情拆分成不同对象,靠对象之间的交互来解决问题
c
语言中,结构体只能定义变量,在cpp
中,结构体不仅可以定义变量,也可以定义函数,将struct
改为class
就变成了类。类成员变量的范围是整个类中。
#include <iostream> #include <cstdlib> using namespace std; struct Student { void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } // _不是必须加, 用来标识成员变量 // 在给成员变量更新值时 好分辨 不用采用其他名字 避免混淆 char _name[20]; char _gender[15]; int _age; }; int main() { struct Student zhangsan; zhangsan.SetStudentInfo("zhang san", "male", 13); zhangsan.PrintStudentInfo(); // c++ 对结构体升级称为了类 可以直接通过类名来声明变量 可以不typedef下 Student lisi; lisi.SetStudentInfo("lisi", "female", 14); lisi.PrintStudentInfo(); return 0; }
// 定义结构体的指针时 可以直接用结构体名来定义 不需要struct 和 typedef #include <iostream> using namespace std; struct ListNode { int val; ListNode* next; }; struct List { int val; struct List* left; struct List* right; }; int main() { ListNode n1; ListNode n2; n1.val = 1; n2.val = 2; n1.next = &n2; n2.next = NULL; ListNode* cur = &n1; while (cur != NULL) { cout << cur->val << " "; cur = cur->next; } cout << endl; List p1; List p2; p1.val = 3; p2.val = 4; p1.left = NULL; p1.right = &p2; p2.left = &p1; p2.right = NULL; List* now = &p1; while (now != NULL) { cout << now->val << " "; now = now->right; } cout << endl; now = &p2; while (now != NULL) { cout << now->val << " "; now = now->left; } cout << endl; return 0; }
将
struct
变为class
在cpp
就成为了类
struct
不加访问限定符,默认是public
.// 下面代码编译正确 说明struct 在不加限定符是 默认是 public #include <iostream> using namespace std; struct Student { void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } char _name[20]; char _gender[15]; int _age; }; int main() { Student s1; s1._age = 20; return 0; }
// 下面代码是错误的 #include <iostream> using namespace std; struct Student { void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } private: char _name[20]; char _gender[15]; int _age; }; int main() { Student s1; s1._age = 20; return 0; }
class
不加访问限定符,默认是private
// 编译后 会报错 // 显示无法访问 private 成员 // private 成员 只能在类中 访问 不同通过创建一个类的对象 通过对象来访问 #include <iostream> using namespace std; class Student { void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } char _name[20]; char _gender[15]; int _age; }; int main() { Student s1; s1._age = 20; return 0; }
// 将成员定义为 public 时 就可以通过类的对象来访问 #include <iostream> using namespace std; class Student { public: void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } char _name[20]; char _gender[15]; int _age; }; int main() { Student s1; s1._age = 20; return 0; }
类的定义:
class className { // 类体: 由成员函数 和成员变量组成 };// 必须有分号 // class 是关键字 // className 是类的名字 // {} 是类的主题 // 类中的元素成为类的成员 // 类中的数据称为类的属性或者成员变量 // 类中的函数成为类的方法或者成员函数
类的两种定义方式:
声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处
理// 声明和定义全部放在类体中 // 在类中定义 // 在类里面定义的函数默认是 inline // 在实际中,一般情况下,短小的函数可以直接在类中定义,长一点的函数声明和定义分离 // 申明就是没有开辟空间 定义开辟了空间 #include <iostream> using namespace std; class Student { public: void SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } char _name[20]; char _gender[15]; int _age; };
声明放在.h文件中,类的定义放在.
cpp
文件中// Student.h #include <iostream> using namespace std; class Student { // 在类中声明 public: void SetStudentInfo(const char* name, const char* gender, int age); void PrintStudentInfo(); char _name[20]; char _gender[15]; int _age; };
// Student.cpp // 在cpp 文件中定义 #include "Student.h" void Student::SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void Student::PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; }
类的访问限定符
访问限定符:
public
: 公有protect
:保护private
:私有访问限定符的说明:
public
修饰的成员在类外可以直接被访问protected
和private
修饰的成员在类外不能直接被访问- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
class
的默认访问权限为private
,struct
为public
(因为struct
要兼容C
)- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++
中struct
和class
的区别
C++
需要兼容C
语言,所以C++
中struct
可以当成结构体去使用C++
中struct
还可以用来定义类struct
的成员默认访问方式是public
,class
的成员默认访问方式是private
类的封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行
交互
cpp
中类封装的方法:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用分离的坏处:太过于自由,没办法管理
// 使用c语言的不封装 #include <cstdio> struct Stack { int* _a; int _top; int _capacity; }; void StackInit(struct Stack* ps) { ps->_a = NULL; ps->_top = 0; ps->_capacity = 0; } void StackPush(struct Stack* ps, int x) { } int StackTop(struct Stack* ps) { } int main() { struct Stack st; StackInit(&st); StackPush(&st, 1); StackPush(&st, 2); StackPush(&st, 3); StackPush(&st, 4); printf("%d\n", StackTop(&st)); // 栈是后进先出 没有规定实现的方式, 可能会有错误 printf("%d\n", st._a[st._top]); printf("%d\n", st._a[st._top-1]); return 0; }
// cpp 的封装 #include <iostream> using namespace std; // 1. 把数据和方法封装到一起, 类里面 // 2. 不想给你访问的 定义为私有 // 3. 想给你访问的 定义为公有 // 一般情况设计类,成员的数据都是私有或者保护的 想给访问的函数是公共的,不想给你访问时私有或保护 class Stack { public: void Init() { } void Push(int x) {} int Top() {} private: int* _a; int _top; int _capacity; }; int main() { Stack st; st.Init(); st.Push(1); st.Push(3); st.Push(2); st.Push(3); // 在使用cpp 时, 无论内部是如何实现的, 使用的方法都不变 cout << st.Top() << endl; return 0; }
面向对象的三大特征:封装、继承、多态
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类外定义类的成员,需要使用作用域解析符(
::
)指明成员属于哪个类域。class Student { // 在类中声明 public: void SetStudentInfo(const char* name, const char* gender, int age); void PrintStudentInfo(); char _name[20]; char _gender[15]; int _age; }; // SetStudentInfo 属于 Student 类域中 void Student::SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void Student::PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; }
类的实例化
实例化:用类的类型创建对象的过程,成为类的实例化
类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
#include <iostream> using namespace std; class Student { // 在类中声明 public: void SetStudentInfo(const char* name, const char* gender, int age); void PrintStudentInfo(); char _name[20]; char _gender[15]; int _age; }; // SetStudentInfo 属于 Student 类域中 void Student::SetStudentInfo(const char* name, const char* gender, int age) { strcpy(_name, name); strcpy(_gender, gender); _age = age; } void Student::PrintStudentInfo() { cout << _name << " " << _gender << " " << _age << " " << endl; } int main() { // a 是Student 类的一个对象 Student a; return 0; }
类对象模型
1.计算类对象的大小
类对象大小的计算和结构一致。结构体计算大小:
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整倍数的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值(vs 8)
结构体总体大小为最大对齐数(每个成员变量都有一个对齐数)的整倍数。
如果嵌套了结构体情况,嵌套的结构体对齐到自己的最大对齐数的整数倍数,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
2.类中对象的存储方式
只保存成员变量,成员函数存放在公共的代码段
计算类的大小
// 类中有成员变量 也有成员函数
// 函数放在公共代码区
#include <iostream>
using namespace std;
class Student
{
public:
void SetStudentInfo(const char* name, const char* gender, int age) {
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo() {
cout << _name << " " << _gender << " " << _age << " " << endl;
}
private:
char _name[20];
char _gender[15];
int _age;
};
int main()
{
Student a;
// _name 0 ~ 19
// _gender 20 ~ 34
// _age 36 ~ 39
// 最大对齐数 4 4的整数倍40
cout << sizeof(a) << endl; // 40
return 0;
}
// 类中有成员函数 但是没有成员变量
// 编译会给他们1 字节 表示类的存在
// 调用函数不是在对象中找,而是在公共的代码区域去找
#include <iostream>
using namespace std;
class Student
{
public:
void SetStudentInfo(const char* name, const char* gender, int age)
{}
void PrintStudentInfo()
{}
};
void test()
{
cout << sizeof(Student) << endl;
}
int main()
{
test();
return 0;
}
// 空类 类中既没有成员变量 也没有成员函数
// 为了表示类的存在 编译会给一个字节用来标识
#include <iostream>
using namespace std;
class Student
{};
void test()
{
cout << sizeof(Student) << endl;
}
int main()
{
test();
return 0;
}
this
指针
介绍
通过类的对象.类的成员函数 就可以使用类的函数,而类在编译的过程中在类中只有成员变量,成员函数在外部,如何通过外部调用内部。
cpp
编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成#include <iostream> using namespace std; class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } // 编译器处理后的结果 //void Print(Date* const this) { // cout << this->_year << "-" << this->_month << "-" << this->_day << endl; //} void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } 编译器处理的结果 //void Init(Date* const this, int year, int month, int day) //{ // this->_year = year; // this->_month = month; // this->_day = day; //} private: int _year; int _month; int _day; }; void test() { Date d1; d1.Init(1949, 10, 1); // 编译器处理后 d1.Init(&d1, 1949, 10, 1) d1.Print(); // d1.Print(&d1) } int main() { test(); return 0; }
#include <iostream> using namespace std; class Date { public: void Print() { cout << this << endl; cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } void Init(int year, int month, int day) { cout << this << endl; this->_year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; // 下列的测试 说明了this 指针是这个元素的地址 cout <<"d1:"<<&d1<< endl; // d1:008FF898 cout << "d2:" << &d2 << endl; // d2:008FF884 d1.Init(1949, 10, 1); // 008FF898 d2.Init(2019, 10, 1); // 008FF884 d1.Print(); // 008FF898 // 1949-10-1 d2.Print(); // 008FF884 // 2019-10-1 Date* p = &d1; p->Print(); // 008FF898 // 1949-10-1 return 0; }
特征
this
指针的类型:* const
, 说明this
指针本身不能修改,因为它是被const
修饰,但this
所指向的内容是可以修改的。- 只能在“成员函数”的内部使用
this
指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this
指针this
指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx
寄存器自动传递,不需要用户
传递this
指针存放在栈中 有些时候也可能存放在寄存器优化#include <iostream> using namespace std; class A { public: void Show() { cout << this << endl; cout << "Show()" << endl; } private: int _a; }; int main() { // 下面的程序能够正常运行 // 虽然讲空指针传了过去 但是里面的操作没有对空指针进行解引用 所以不会报错 // 空指针的解引用不会报编译错误 // 32 位有 2 ^ 32 个地址,每个地址是一个字节 // 空指针是一个存在的地址 // 空指针是最开始的位置,不能对这些位置进行访问,用来初始化的 A* p = nullptr; p->Show(); }
#include <iostream> using namespace std; class A { public: void PrintA() { cout << _a << endl;// this->_a 发生了对this指针的解引用 会发生错误 } private: int _a = 10; }; int main() { // 下面程序会发生 运行时的错误 // 因为将空指针传进去 会进行解引用 发生错误 A* p = nullptr; p->PrintA(); return 0; }
类的默认成员函数
在一个类中,如果这个类中什么都不存在,那么就简称为空类。
任何类在我们不写的情况下,会自动生成6个默认成员函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fi6xX2Al-1661780257186)(D:/picture/image-20220521093112095.png)]
构造函数
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员
都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
特性
- 函数名于类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定
义编译器将不再生成 - 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参
构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
#include <iostream>
using namespace std;
class Stack
{
public:
Stack() {
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
// 类的成员变量是自定义类型 可以不写构造函数 用默认生成的
void push() {
}
int pop() {
return 1;
}
private:
Stack _str1;
Stack _str2;
};
class Data
{
public:
/**
* 默认构造函数 我们不写 编译器会自动生成一个默认的无参构造的 只要我们写了 就不会生成
* 内置类型/ 基本类型: int char double 指针
* 自定义类型: class/ struct 去定义类型对象
* 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会做处理(调用自定义类型的默认构造函数)
*
* 如果一个类中的成员全是自定义类型,我们可以用默认生成的函数。
* 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数
*
*/
Data() {
this->_year = 2022;
this->_month = 10;
this->_day = 13;
}
Data(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
/**
* Data(int year, int month, int day) 和 Data() 构成了函数重载
*
* Data(int year = 1, int month = 1, int day = 1) 和 Data() 不能同时存在 一般情况保存前面的
*
*/
void Print() {
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
/**
* c++11 打的补丁 针对编译器自己生成默认成员函数不初始化的问题
*
* int _year = 2022;
* int _month = 12;
* int _day = 23;
*
*/
};
int main()
{
// Data d1; 无参时只能这的写
// Data d1(); 无参数不能这的写
Data d3(11, 11, 1);
d3.Print();
return 0;
}
#include <iostream>
using namespace std;
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
/**
* 一般情况下 一个cpp 类 都要自己写构造函数 一般只有少数可以让编译器默认生成
* 1. 类里面成员是自定义类型成员,并且这些成员都提供了默认构造函数
* 2. 如果有内置类型成员,声明时给了缺省值
*
*/
int main()
{
return 0;
}
析构函数
概念
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特性
- 析构函数名是在类名前加上字符
~
- 无参数无返回值
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
#include <iostream>
using namespace std;
class Stack
{
public:
Stack() {
this->_a = nullptr;
this->_top = _capacity = 0;
}
~Stack() {
free(this->_a);
this->_a = nullptr;
this->_top = this->_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push() {
}
int pop() {
return 1;
}
private:
Stack _str1;
Stack _str2;
};
class Data
{
public:
Data() {
this->_year = 2022;
this->_month = 10;
this->_day = 13;
}
Data(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
/**
* 析构函数完成资源清理
* 栈的空间 malloc new 出来的
* 日期类不需要写 没有资源需要清理 栈需要写 有资源需要处理
* 对象生命周期到了之后,自动调用
* 构造函数相当于 Init 析构函数相当于 Destory
* 为什么有构造函数 和析构函数?
* 觉得c语言不好 要加强 c 语言显示调用 Init 可能会忘
* 写一个函数,会自动调用,只要写了构造函数,就一定会初始化
*
* 基本类型不处理 自定义类型处理(调用它的析构函数)
*/
~Data() {
// 用来检验是否调用
//
cout << "~Data" << endl;
}
void Print() {
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
/**
* st1 先构造 st2 后构造
* st2 先析构 st1 后析构
* 在栈中存放, 服从栈的先进后出,后进先出的规律
*
*/
Stack st1;
Stack st2;
Data d3(11, 11, 1);
d3.Print();
return 0;
}
拷贝构造函数
概念
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象
创建新对象时由编译器自动调用。
特性
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
-
若未显示定义,系统生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
#include <iostream>
using namespace std;
class Stack
{
public:
Stack() {
this->_a = nullptr;
this->_top = _capacity = 0;
}
~Stack() {
free(this->_a);
this->_a = nullptr;
this->_top = this->_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push() {
}
int pop() {
return 1;
}
private:
Stack _str1;
Stack _str2;
};
class Data
{
public:
Data() {
this->_year = 2022;
this->_month = 10;
this->_day = 13;
}
Data(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
/**
* 拷贝构造的时候 如果不用引用 就会无穷
*
*
* 同类型的对象初始化 调用拷贝构造
* 如果不是引用会 无限循环 Data d2(d1); Data(Data d) 传d1 的时候会调拷贝构造 调拷贝构造的时候又需要传参 传参需要拷贝构造 然后陷入循环
* Data(Data d1) Data(Data& d) 调用引用可解决 最好加const 保护 Data(const Data& d)
*
* 默认生成的构造函数是浅拷贝
* 为什么要用拷贝构造---> 因为有自定义类型构造时需要深拷贝, 浅拷贝会崩
* 比如 析构时 如果浅拷贝话可能会导致同一块空间释放两次
*
*
*/
// 内置类型完成值拷贝
// 自定义类型会调用它的拷贝构造
// 一般的类 自己生成的拷贝构造就够用了
// 只有像Stack 这样的类,自己直接管理资源 需要实现深拷贝
Data(const Data& d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
~Data() {
cout << "~Data" << endl;
}
void Print() {
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(11, 11, 1);
Data d2(d1);
return 0;
}
赋值操作符的重载
运算符重载
C++
为了增强代码的可读性引入了运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator
后面接需要重载的运算符符号 operator==
函数原型:返回值类型 operator
操作符(参数列表) bool operator==(Data d)
注:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参 .*
、::
、sizeof
、?:
、.
注意以上5个运算符不能重载
赋值运算符的重载
赋值运算符的特点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
日期类的实现
// Data.h
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1) {
if (year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= getMonthDay(year, month)) {
this->_year = year;
this->_month = month;
this->_day = day;
} else cout << "日期非法" << endl;
}
Data(const Data& d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// inline 不支持声明和定义分别放到.h 和 .cpp
// 成员函数中要成为 inline 最好直接在类里面定义
// 类里面默认就是inline
//
bool operator==(const Data& d);
bool operator<(const Data& d);
bool operator<=(const Data& d);
bool operator!=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool isLeapYear(int year) {
return(year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
int getMonthDay(int year, int month) {
assert(year >= 0 && month >= 1 && month <= 12);
static int monthDayArray[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && isLeapYear(year)) return 29;
return monthDayArray[month];
}
Data operator+(int day);
Data& operator=(const Data& d);
Data& operator+=(int day);
Data operator-(int day);
Data& operator-=(int day);
Data& operator++(); // 前置++
Data operator++(int); // 后置++
int operator-(const Data& d);
private:
int _year;
int _month;
int _day;
};
// Data.cpp
#include "Data.h"
bool Data::operator==(const Data& d) {
return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
bool Data::operator<(const Data& d) {
return (this->_year < d._year || (this->_year == d._year && this->_month < d._month) ||
(this->_year == d._year && this->_month == d._month && this->_day < d._day));
}
bool Data::operator<=(const Data& d) {
return *this < d || *this == d;
}
bool Data::operator!=(const Data& d) {
return !(*this == d);
}
bool Data::operator>(const Data& d){
return !(*this <= d);
}
bool Data::operator>=(const Data& d) {
return !(*this < d);
}
/**
* 返回类型不能是& 函数结束时空间销毁, 就成为野指针,
*
*/
Data Data::operator+(int day) {
if (day < 0) return *this - -day;
Data ret(*this);
ret._day += day;
while(ret._day > getMonthDay(ret._year,ret._month)) {
ret._day -= getMonthDay(ret._year, ret._month);
ret._month++;
if (ret._month == 13) {
ret._year++;
ret._month = 1;
}
}
return ret;
}
Data& Data::operator=(const Data& d) {
if (this != &d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
Data& Data::operator+=(int day) {
if (day < 0) return *this -= -day;
_day += day;
while (_day > getMonthDay(_year, _month)) {
_day -= getMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
/**
* += 复用+
* Data& Data::operator+=(int day) {
* *this = *this + day;
* return *this;
* }
*
*/
/**
* + 复用 +=
* Data Data::operator+(int day) {
* Data ret(*this);
* ret += day;
* return ret;
* }
*
*/
/**
* 调用一次 + 有两次拷贝构造 += 没有拷贝构造
* + 调用 += 更优
*
* += 调用 + 时: + 有两次拷贝构造 += 也有两次拷贝构造
* + 调用 += 时: + 有两次拷贝构造 += 没有拷贝构造
*
*/
Data Data::operator-(int day) {
Data ret(*this);
return ret -= day;
}
Data& Data::operator-=(int day) {
if (_day < 0) return *this += -day;
_day -= day;
while (_day <= 0) {
_month--;
if (_month == 0) {
_month = 12;
_year--;
}
_day += getMonthDay(_year, _month);
}
return *this;
}
Data& Data::operator++() {
return *this += 1;
}
Data Data::operator++(int) {
Data ret(*this);
*this += 1;
return ret;
}
int Data::operator-(const Data& d) {
int flag = 1;
Data max = *this;
Data min = d;
if (*this < d) {
min = *this;
max = d;
flag = -1;
}
int n = 0;
while (min != max) {
n++;
++min;
}
return n * flag;
}
void Func(const Data& d) {
d.Print();
/*
* 错误的
* void Print() 中第一个参数的类型是 Data* const this
*
* d.Print() --- d.Print(&d);
* 而&d的类型是 const Data*
* 如果传过去的话就会把权限扩大 错误
*
* 解决方法
*
* void Print() const ---- 就相当于 const Data* const this
*/
// 成员函数中不修改成员变量的成员函数 都可以加上const
// 普通对象和const 对象都可以调用
}
取地址操作符的重载(非const
)
- const对象可以调用非const成员函数吗?
- 不可以,权限的放大
- 非const对象可以调用const成员函数吗?
- 可以,权限的缩小
- const成员函数内可以调用其它的非const成员函数吗?
- 不能,权限的放大
- 非const成员函数内可以调用其它的const成员函数吗?
- 可以,权限的缩小
Data* operator&() {
return this;
}
// 几乎不需要自己写
// 除了不想让别人拿到真实地址
取地址运算符的重载(const
)
const Data* operator&() const {
return this;
}
// 几乎不需要自己写
// 除了不想让别人拿到真实地址
输入、输出的重载
// 声明
//#pragma once
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Data
{
friend std::ostream& operator<<(std::ostream& out, const Data& d);
friend std::istream& operator>>(std::istream& in, Data& d);
public:
Data(int year = 1, int month = 1, int day = 1) {
if (year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= getMonthDay(year, month)) {
this->_year = year;
this->_month = month;
this->_day = day;
} else cout << "日期非法" << endl;
}
Data(const Data& d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// inline 不支持声明和定义分别放到.h 和 .cpp
// 成员函数中要成为 inline 最好直接在类里面定义
// 类里面默认就是inline
//
bool operator==(const Data& d);
bool operator<(const Data& d);
bool operator<=(const Data& d);
bool operator!=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool isLeapYear(int year) {
return(year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
int getMonthDay(int year, int month) {
assert(year >= 0 && month >= 1 && month <= 12);
static int monthDayArray[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && isLeapYear(year)) return 29;
return monthDayArray[month];
}
Data operator+(int day);
Data& operator=(const Data& d);
Data& operator+=(int day);
Data operator-(int day);
Data& operator-=(int day);
Data& operator++(); // 前置++
Data operator++(int); // 后置++
int operator-(const Data& d);
private:
int _year;
int _month;
int _day;
};
// 定义
#include "Data.h"
bool Data::operator==(const Data& d) {
return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
bool Data::operator<(const Data& d) {
return (this->_year < d._year || (this->_year == d._year && this->_month < d._month) ||
(this->_year == d._year && this->_month == d._month && this->_day < d._day));
}
bool Data::operator<=(const Data& d) {
return *this < d || *this == d;
}
bool Data::operator!=(const Data& d) {
return !(*this == d);
}
bool Data::operator>(const Data& d){
return !(*this <= d);
}
bool Data::operator>=(const Data& d) {
return !(*this < d);
}
/**
* 返回类型不能是& 函数结束时空间销毁, 就成为野指针,
*
*/
Data Data::operator+(int day) {
if (day < 0) return *this - -day;
Data ret(*this);
ret._day += day;
while(ret._day > getMonthDay(ret._year,ret._month)) {
ret._day -= getMonthDay(ret._year, ret._month);
ret._month++;
if (ret._month == 13) {
ret._year++;
ret._month = 1;
}
}
return ret;
}
Data& Data::operator=(const Data& d) {
if (this != &d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
Data& Data::operator+=(int day) {
if (day < 0) return *this -= -day;
_day += day;
while (_day > getMonthDay(_year, _month)) {
_day -= getMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
/**
* += 复用+
* Data& Data::operator+=(int day) {
* *this = *this + day;
* return *this;
* }
*
*/
/**
* + 复用 +=
* Data Data::operator+(int day) {
* Data ret(*this);
* ret += day;
* return ret;
* }
*
*/
/**
* 调用一次 + 有两次拷贝构造 += 没有拷贝构造
* + 调用 += 更优
*
* += 调用 + 时: + 有两次拷贝构造 += 也有两次拷贝构造
* + 调用 += 时: + 有两次拷贝构造 += 没有拷贝构造
*
*/
Data Data::operator-(int day) {
Data ret(*this);
return ret -= day;
}
Data& Data::operator-=(int day) {
if (_day < 0) return *this += -day;
_day -= day;
while (_day <= 0) {
_month--;
if (_month == 0) {
_month = 12;
_year--;
}
_day += getMonthDay(_year, _month);
}
return *this;
}
Data& Data::operator++() {
return *this += 1;
}
Data Data::operator++(int) {
Data ret(*this);
*this += 1;
return ret;
}
int Data::operator-(const Data& d) {
int flag = 1;
Data max = *this;
Data min = d;
if (*this < d) {
min = *this;
max = d;
flag = -1;
}
int n = 0;
while (min != max) {
n++;
++min;
}
return n * flag;
}
std::ostream& operator<<(std::ostream& out, const Data& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
std::istream& operator>>(std::istream& in, Data& d) {
in >> d._year >> d._month >> d._day;
return in;
}
- 全局函数不能在 .h 中定义 展开的时候 会造成连接冲突
- 声明函数时,可以在最前面加上一个 friend 表示友元函数 可以访问私有的数据域
- 类里面定义的默认是内联,不会放入符号表中
- 最好 .h 声明 .cpp 定义
前两部分细节补充
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括
号中的初始值或表达式。
注:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使
用初始化列表初始化- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
#include <iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 0) {
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
/**
* 当啥也不写时 会调用初始化列表 自定义类型调用自己的构造函数 内置类型用数据域中值定义
*
*/
MyQueue() {
}
private:
Stack _st1;
Stack _st2;
size_t _size = 0; // 这个初始化值是给初始化列表用的
};
class A
{
public:
A(int a)
: _a(a)
{ }
private:
int _a;
};
class Date
{
public:
// 函数体内初始化
// Date(int year = 1, int month = 1, int day = 1) {
// _year = year;
// _month = month;
// _day = day;
// }
// 初始化列表
Date(int year = 1, int month = 1, int day = 1, int a = 1)
:_year(year)
,_month(month)
,_day(day)
,_aa(a)
{ }
/**
* 初始化列表的意义:
* 初始化列表可以认为是成员变量定义的地方
* 有些类型只能在定义时初始化, const int _n; int& _ref; 其中_n _ref 只能在初始化列表中定义
* 建议尽量在初始化列表中初始化
*/
private:
};
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
}
int main() {
A aa(1);
aa.Print();
// 1 随机值
// 按照声明的顺序初始化 先初始化_a2 再初始化 _a1
}
explicit 关键字
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
#include <iostream>
#include <assert.h>
#include <stdlib.h>
using namespace std;
class Stack
{
public:
Stack(int capacity = 0) {
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
MyQueue() {
}
private:
Stack _st1;
Stack _st2;
size_t _size;
};
class Date
{
public:
Date(int year)
:_year(year) {
cout << "aaa" << endl;
}
private:
int _year;
};
void Func(const string& s) {
}
int main()
{
Date d1(20);// 构造
Date d2 = 30;
/**
* d2 = 30 构造 + 拷贝构造 ---> 有些编译器 会优化 合二为一
* 构造函数加上关键字 explicit 不允许这样的转换
*
*/
const Date& d3 = 2022; // 引用的临时变量 具有常性
Func("hello"); // 单参数构造 可以使字符串传过去
string s("hello");
string a = "hello";
return 0;
}
静态成员
/**
* 统计A类型对象创建了多少个
*/
#include <iostream>
using namespace std;
class A
{
public:
A() {
++_count1;
}
A(const A& aa) {
++_count2;
}
// private:
static int _count1;
static int _count2;
};
/**
* 静态变量 在类中声明 在类外定义
* 访问方式:
* 通过某个对象
* 通过某个类域
*/
int A::_count1 = 0;
int A::_count2 = 0;
A Func(A a) {
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = Func(a1);
cout << A::_count1 << endl; // 1
cout << A::_count2 << endl; // 2
}
以下代码共调用多少次拷贝构造函数:(D)
Weight f(Weight u) { Weight v(u); Weight W = v; return W; } int main() { Weight x; Weight y = f(f(x)); }
A. 1
B. 3
C. 5
D. 7
class Weight { public: Weight() { cout << "Weight()" << endl; } Weight(const Weight& w) { cout << "Weight(const Weight& W)" << endl; } ~Weight() { cout << "~Weight()" << endl; } private: } Weight f(Weight u) { Weight v(u); Weight W = v; return W; } int main() { Weight(); // 匿名对象 生命周期只在这一行 // 构造 + 拷贝构造 会合二为一 // 连续的 拷贝构造 + 拷贝构造 会优化 合二为一 Weight x; Weight y = f(f(x)); }
描述
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0 < n ≤ 200 0 < n \le200 0<n≤200
进阶: 空间复杂度 O(1) ,时间复杂度 O(n)
构建n个类,每个类初始化的时候 在构造函数中进行加 将变量设置为静态 最后返回
class Sum {
public:
Sum() {
_ret += _i;
_i++;
}
static int GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetRet();
}
};
友元
友元函数
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend
关键字。 - 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用
const
修饰 - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
/**
* ostream& operator<<(ostream& out);
* Date d;
* 如果流插入操作符在类中这样重载, 则使用时 d << cout;
* 与常规不同 cout << d;
* 如果要写成常规, 则需要第一个参数为cout
* 所以需要写在外面 而在外部不能访问私有数据域 所以设置友元函数
*/
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
友元类
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
Date 类是Time 类的友元类,Date类可以访问Time类的私有数据域
#include <iostream>
using namespace std;
class Date; // 前置声明 表明Date 是一个类
class Time
{
friend void Func(const Date& d, const Time& t);
friend class Date;
public:
Time(int hour = 1, int minute = 1, int second = 1)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
friend void Func(const Date& d, const Time& t);
public:
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
void setTimeOfDate(int hour, int minute, int second) {
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
/**
* 一个友元函数 可以是多个类的友元函数
*/
void Func(const Date& d, const Time& t) {
cout << d._year << endl;
cout << t._hour << endl;
}
内部类
推荐程序低耦合
概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。
外部类对内部类没有任何优越的访问权限
内部类就是外部类的友元类。
注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。
但是外部类不是内部类的友元
特性:
- 内部类可以定义在外部类的
public
、protected
、private
都是可以的- 注意内部类可以直接访问外部类中的
static
、枚举成员,不需要外部类的对象/类名sizeof(外部类)=外部类
,和内部类没有任何关系
#include <iostream>
using namespace std;
/**
* 内部类
* B 是 A 的内部类
* B 是 A 的友元
*
* 只有通过A 来使用内部类
*/
class A {
private:
static int k;
int h;
public:
class B {
public:
void foo(const A& a) {
cout << k << endl;
cout << a.h << endl;
}
private:
int _b;
};
};
int A::k = 0;
int main()
{
A aa;
A::B bb;
cout << sizeof(A) << endl;
// 计算外部类的大小时,不算内部类的大小
return 0;
}
继承
继承的概念及定义
概念:
继承(inheritance
)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
图书管理系统
角色类:学生、代课老师、行政老师、保安、保洁、后勤…
class student { string _name; string _id; string _tel; string _address; }; class student { string _name; string _id; string _tel; string _address; };
有些数据和方法每个角色都有的 — 设计重复了
有些数据和方法每个角色独有的
使用继承来提取:
class Person { string _name; string _tel; string _address; int _age; }; class Student: public Person { // .. } // 派生类 继承方式 基类 class teacher: public Person { // ... } // 继承是类设计角度的复用 // Person 是父类 也称为基类 // Student 是子类 也称派生类
继承关系和访问限定符
继承方式:
public
继承、protected
继承、private
继承访问限定符:
public
访问、protected
访问、private
访问
类成员/继承方式 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的public 成员 | 派生类的public 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected 成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
-
基类
private
成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 -
基类
private
成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected
。可以看出保护成员限定符是因继承才出现的。 -
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),
public
>protected
>private
。 -
使用关键字
class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,不过最好显示的写出继承方式。 -
在实际运用中一般使用都是
public
继承,几乎很少使用protetced
/private
继承,也不提倡使用protetced
/private
继承,因为protetced
/private
继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
protected
private
成员对于基类 – 一样的,类外面不能访问,类里面可以访问
protected
private
成员对于派生类 –private
成员不能使用protected
成员类里面可以使用
基类和派生类对象赋值转换
-
派生类对象可以赋值给基类的对象/基类的指针/基类的引用 (建立在公有继承基础上)
这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
class Person { public: // protected : string _name; string _sex; int _age; }; class Student: public Person { public : int _No ; }; void Test () { Person p; Student s; // 子类对象给父类 对象/ 指针/ 引用 语法天然支持 没有类型转换 p = s; Person& rp = s; // 子类中父类那部分的别名 对rp 的修改会影响s Person* ptrp = &s; // 指向父类 切出来的那一部分 对ptrp 的修改会影响s }
-
基类对象不能赋值给派生类对象。
class Person { public: string _name; string _sex; int _age; }; class Student: public Person { public : int _No ; }; void Test () { Person p; Student s; s = p; }
-
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。
但是必须是基类的指针是指向派生类对象时才是安全的。
这里基类如果是多态类型,可以使用
RTTI
(Run-Time Type Information
)dynamic_cast
来进行识别后进行安全转换。
继承中的作用域
-
在继承体系中基类和派生类都有独立的作用域。
-
类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用
基类::基类成员
显示访问)class Person { protected: string _name = "张山"; int _num = 111; }; class Student: public Person { public: void Print() { cout << " 姓名" << _name << endl; // cout << " 学号" << _num << endl; cout << " 学号" << Person::_num << endl; } protected: int _num = 999; };
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
class A { public: void fun() { cout << "A::func()" << endl; } }; class B : public A { public: void fun() { cout << "B::func()->" <<i<<endl; } } void test() { B b; b.fun(); // 调用自己 b.A::fun(); // 指定作用域 调用基类的 }
// A::fun 和 B::fun 的关系 -> 隐藏 // 函数重载要求在同一作用域 class A { public: void fun() { cout << "A::func()" << endl; } }; class B: public A { public: void fun(int i) { cout << "B::func()->" <<i<<endl; } }
-
注意在实际中在继承体系里面最好不要定义同名的成员。
派生类的默认成员函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的
operator=
必须要调用基类的operator=
完成基类的复制。 - 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成
destrutor()
,所以父类析构函数不加virtual
的情况下,子类析构函数和父类析构函数构成隐藏关系。
class Person
{
public :
Person(const char* name = "peter")
: _name(name)
{
cout<<"Person()" <<endl;
}
Person(const Person& p)
: _name(p._name)
{
cout<<"Person(const Person& p)" <<endl;
}
Person& operator=(const Person& p )
{
cout<<"Person operator=(const Person& p)"<< endl;
if (this != &p)
_name = p ._name;
return *this ;
}
~Person()
{
cout<<"~Person()" <<endl;
}
protected :
string _name ; // 姓名
};
class Student : public Person
{
public :
// 构造函数
// 按照声明的顺序初始化 会认为父类在前
Student(const char* name = "张三", int num = 11)
:Person(name)
,_num(num)
{}
// 拷贝构造 涉及深拷贝时需要自己写
Student(const Student& s)
:Person(s)
,_num(s._num)
{}
// 赋值
Student& operator=(const Student& s)
{
if (this != &s)
{
// 需要显示的指定
Person::operator=(s);
_num = s._num;
}
return *this;
}
// 析构
// 父子类的析构函数构成隐藏关系 原因: 多态的需要
// 为了保证析构顺序, 先子后父, 子类的析构函数完成之后 会自动调用父类析构函数 不需要我们显示调用
~Student()
{
// Person::~Person();
}
protected :
int _num ; //学号
};
void test
{
// 子类构造函数的原则:
// 1. 调用父类构造函数初始化继承自父类成员
// 2. 自己再初始化自己的成员
// 析构、拷贝构造、复制重载 也类似
Student s;
}
如何设计一个不能继承的类?
将父类的构造函数私有化
class A { public: statci A CreaeObj() { return A(); } private: A() {} }; class B: public A { } int main() { B a1; A a2 = A::CreaeObj(); }
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student: public Person
{
// 把这句加进去不会报错
public:
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
继承与静态成员
关于静态成员:基类中有静态成员,派生类共享一个静态成员
// 统计一个类被创建了多少次
class Person
{
public :
Person ()
{
++ _count ;
}
protected :
string _name ;
public :
static int _count;
};
int Person::_count = 0;
class Student: public Person
{
protected :
int _stuNum;
};
class Graduate : public Student
{
protected :
string _seminarCourse;
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person ::_count << endl;
Student ::_count = 0;
cout << " 人数 :" << Person ::_count << endl;
}
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:菱形继承有数据冗余和二义性的问题
解决方案:虚拟继承可以解决菱形继承的二义性和数据冗余的问题 virtual
虚拟继承原理:
class A { public: int _a; }; class B: virtual public A { public: int _b; }; class C: virtual public A { public: int _c; }; class D: public B, public C { public: int _d; }; /* A B C D */ int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; // 采用相同模式处理 B b; b._a = 10; b._b = 20; B* ptr1 = &d; B* ptr2 = &b; cout << ptr1->_a << endl; cout << ptr2->_a << endl; cout << ptr1->_b << endl; cout << ptr2->_b << endl; return 0; }
多开一个空间,用来存放地址,这个地址的下一个位置的值是偏移量,叫做虚基表指针,A叫做虚基类。
cpp
的缺陷:多继承 – 产生菱形继承 — 数据冗余河二意性 — 如何解决(虚继承) – 找虚基类 – 找指针 – 找表 – 找偏移量
补充图:分析每个变量在内存中的位置
组合和继承
class A
{
//...
};
// 继承 is- a 的关系
// 人 -> 学生 学生是人
// 狗 -> 动物 狗是动物
// 宝马 -> 车 宝马是车
class B: public A
{};
// A对象公有成员B可以直接使用
// A对象保护成员B可以直接使用
class C
{};
// 组合 has- a 的关系
// 车 -> 轮胎 车有轮胎
// 头 -> 眼睛 头有眼睛
class D
{
C _c;
};
// C对象公有成员D可以直接使用
// C对象保护成员D不能直接使用
适合
is-a
的关系,最好用继承, 是一种白盒,知道具体实现的细节。基类的改变,对派生类有很大的影响适合
has-a
的关系,最好用组合 是一中黑盒, 不知道具体实现的细节。基类的改变,不会对派生类产生很大的影响。有助于保持类的封装。都可以,最好用组合。
尽量做到高内聚,低耦合。
UML
类图
继承的总结和反思
-
有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。
所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的面对对象语言都没有多继承,如Java。
-
继承和组合
public
继承是一种is-a
的关系。也就是说每个派生类对象都是一个基类对象。- 组合是一种
has-a
的关系。假设B
组合了A
,每个B
对象中都有一个A对象。
练习题
-
什么是菱形继承?菱形继承的问题是什么?
-
什么是菱形虚拟继承?如何解决数据冗余和二义性的?
-
继承和组合的区别?什么时候用继承?什么时候用组合?
多态
多态的概念
多态:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
BuyTicket()
普通人、学生、军人不同类型的人调用函数时会产生不同的结果
Pay()
普通用户、一级
VIP
(88折) 、二级VIP
(66折) 、三级VIP
(55折),形态结果是不一样的。
多态的定义及实现
多态构成的条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class Person {
public:
Person(const char* name)
:_name(name)
{}
virtual void BuyTicket() {
cout << _name << "买票-全价" << endl;
}
protected:
string _name;
};
class Student: public Person {
public:
Student(const char* name)
:Person(name)
{}
virtual void BuyTicket() {
cout << _name << "买票-半价" << endl;
}
};
class Soldier: public Person {
public:
Soldier(const char* name)
:Person(name)
{}
virtual void BuyTicket() {
cout << _name << "买票-三折" << endl;
}
};
// 多态两个条件:
// 1. 子类虚函数重写的父类虚函数(重写: 三同(函数名 + 参数 + 返回值) + 虚函数)
// 2. 父类指针或者引用去调用虚函数
void Pay(Person* ptr) {
ptr->BuyTicket();
}
int main()
{
int option= 0;
do {
cout << "请选择身份: ";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
cout << "请输入你的名字" << endl;
string name;
cin >> name;
switch (option) {
case 1:
Pay(new Person(name.c_str()));
break;
case 2:
Pay(new Student(name.c_str()));
break;
case 3:
Pay(new Soldier(name.c_str()));
break;
default:
cout << "输入错误" << endl;
break;
}
cout << "====================================" << endl;
} while (option != -1);
return 0;
}
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
一般虚函数的使用
class A
{};
class B: public A
{};
// 虚函数重写对返回值有一个列外, 协变,父子关系指针和引用
class Person {
// 父类不写virtual 不是虚函数
virtual A* f() {
cout << "virtual A* ()" << endl;
return nullptr;
}
/*
virtual A& f() {
cout << "virtual A& ()" << endl;
return A();
}
*/
};
class Student: public Person {
// 子类的虚函数没有写 virtual, f() 依旧是虚函数, 因为先继承了父类函数接口声明
// 重写父类虚函数实现
virtual B* f() {
cout << "virtual B* ()" << endl;
return nullptr;
}
/*
virtual B& f() {
cout << "virtual B& ()" << endl;
return B();
}
*/
};
int main()
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
}
析构虚函数的使用
class Person
{
public:
~Person() {
cout << "~Person()" << endl;
}
};
class Student: public Person
{
public:
// 隐藏关系
~Student() {
cout << "~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
/*
~Student()
~Person()
~Person()
*/
return 0;
}
class Person
{
public:
// 父类的析构函数最好设置为virtual
virtual ~Person() {
cout << "~Person()" << endl;
}
};
class Student: public Person
{
public:
// 重写关系
// 三同 函数名 参数 返回值
// 对于普通对象没有影响
~Student() {
cout << "~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
/*
~Student()
~Person()
~Person()
*/
Person* ptr1 = new Person;
delete ptr1; // ptr1->destructor + operator delete(ptr1)
Person* ptr2 = new Student2;
delete ptr2;// ptr2->destructor + operator delete(ptr2)
return 0;
}
加上虚函数之后类的大小
加上虚函数之后类会变大,因为要存放虚基表
class Base
{
public:
virtual void Func1() {
cout << "Func1" << endl;
}
private:
int _b = 1;
};
int main()
{
// 虚函数在类中 会加一个虚基表
cout << sizeof(Base) << endl; // 8
return 0;
}
不让虚函数进行重写
class Car
{
public:
// 加上final之后 不能被重写
virtual void Drive() final
{}
};
class BMW: public Car
{
public:
virtual void Drive() {
cout << "BMW - 舒适" << endl;
}
};
class Car final
{
public:
virtual void Drive()
{}
};
class BMW: public Car
{
public:
virtual void Drive() {
cout << "BMW - 舒适" << endl;
}
};
/*
final() 的作用:
1. 修饰虚函数,表示虚函数不能被重写
2. 修饰类, 表示类不能被继承 最终类
*/
虚函数检查完成重写 --> 关键字 override
/*
override 写在子类中, 要求严格检查是否完成重写, 如果没有报错
*/
// 1. 会正常运行
class Car{
public:
virtual void Drive()
{}
};
class Benz: public Car {
public:
virtual void Drive() override {
cout << "Benz-舒适" << endl;
}
};
// 2. 会正常运行
class Car{
public:
virtual void Drive()
{}
};
class Benz :public Car {
public:
void Drive() override {
cout << "Benz-舒适" << endl;
}
};
// 3. 会报错
class Car{
public:
void Drive()
{}
};
class Benz :public Car {
public:
virtual void Drive() override {
cout << "Benz-舒适" << endl;
}
};
// 4.会报错
class Car{
public:
void Drive()
{}
};
class Benz :public Car {
public:
void Drive() override {
cout << "Benz-舒适" << endl;
}
};
// 5.正常运行
class Car{
public:
virtual void Drive(int i)
{}
};
class Benz :public Car {
public:
virtual void Drive(char ch) override {
cout << "Benz-舒适" << endl;
}
};
重载/重写(覆盖)/重定义(隐藏)的对比
重载:
- 两个函数在同一作用域
- 函数名/ 参数 不同
重写(覆盖):
- 两个函数分别在基类和派生类的作用域
- 函数名 /参数/ 返回值必须相同(协变除外)
- 两个函数必须是虚函数
重定义(隐藏):
- 两个函数分别在基类和派生类的作用域
- 函数名相同
- 两个基类和派生类的同名函数不够成重写就是重定义
- 同名的成员变量也是隐藏关系
抽象类
纯虚函数:在虚函数的后面写上 = 0
,则这个函数为纯虚函数。
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类)。
抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
// 抽象类
// 在现实一般没有具体对应的实体
// 不能实例化出对象
// 子类需要重写才能实例化出对象
class Car
{
public:
virtual void Drive() = 0; // 纯虚函数
/*
纯虚函数的实现没有意义, 没有人能调用它
*/
};
// 不能实例化出对象
class BMW :public Car
{};
// 可以实例化出对象
class Benz :public Car
{
public:
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
/* 实现继承:
普通函数的继承是一种实现继承。
派生类继承了基类函数,可以使用函数,继承的是函数的实现。
接口继承:
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
*/
多态的原理
虚函数表
在加了虚函数的类中, 大小会发生变化, 有一个虚基指针。指向一块表,表中存放虚函数的地址。
虚函数表简称虚表
虚函数表指针简称虚表指针
// 大小为8
class Base
{
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
/*
__vfptr: 指针数组, 里面存放虚函数的指针
_b
*/
class Base
{
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
// Derive大小为12
class Derive: public Base
{
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
}
/*
__vfptr: 对重写了的虚函数地址进行更新, 其余不变
_b
_d
*/
/*
虚函数的重写 -- 语法层 -- 派生类对继承基类虚函数实现进行了重写
虚函数的覆盖 -- 原理层 -- 子类的虚表,拷贝父类虚表进行了修改,覆盖重写哪个虚函数
多态调用的实现,依靠运行时,去指向对象的虚表中查调用函数的地址
*/
/*
多态调用: 运行时决议 -- 运行时确定调用函数的地址 查虚函数表
普通调用: 编译时决议 -- 编译时确定调用函数的地址 符号表
*/
/*
父类赋值给子类对象,也可以切片, 为什么实现不了多态?
三种切片的不同:
Base b;
Derive d;
对象:
Base b = d;
对象切片的时候, 子类只会拷贝成员给父类对象,不会拷贝虚表指针,拷贝就混乱了
父类对象中到底是父类的虚表指针还是子类虚表指针都有可能?
那么下面调用父类的虚函数还是子类的虚函数?就不确定
切片过指向子类 没有切片过指向父类
指针:
Base* ptr = &d;
引用:
Base& ref = d;
*/
单继承和多继承关系中的虚函数表
单继承中的虚函数表
虚函数都要进虚表。
class Base
{
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
void Func3() {
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
void Func3() {
cout << "Derive::Func3()" << endl;
}
virtual void Func4() {
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
// 取内存值,打印并调用,确认是否是func4
//typedef void(*)() V_FUNC; // 报错
typedef void(*V_FUNC)(); // V_FUNC是函数指针
// 打印虚表
// V_FUNC* a === V_FUNC a[]
void PrintVFTable(V_FUNC* a)
{
printf("vfptr:%p\n", a); // 检验虚表正确不
// vs 可以写的灵活一些
for (size_t i = 0; a[i] != nullptr; ++i) {
printf("[%d]:%p->", i, a[i]); // a[i] 是函数的地址
V_FUNC f = a[i];
f(); // 调用函数就会打印函数的信息
}
}
int c = 2; // 全局
int main()
{
//PrintVFTable((V_FUNC*)(*((int*)&d)));
// 指针之间可以随意相互随便转换
// &d 找到d 的地址
// (int*)&d 把地址强制转换为int*
// *((int*)&d) 读取前四个字节,找到虚表的地址
// (V_FUNC*)(*((int*)&d)) 把读取到的内容转换为函数指针
Base b1;
Base b2;
Base b3;
Base b4;
// 同一个类型的虚表是一样的,应该永久存储
PrintVFTable((V_FUNC*)(*((int*)&b1)));
PrintVFTable((V_FUNC*)(*((int*)&b2)));
PrintVFTable((V_FUNC*)(*((int*)&b3)));
PrintVFTable((V_FUNC*)(*((int*)&b4)));
// 虚表存在哪个区域
// 虚表,一个类型,公共一个虚表,所以这个类型对象都存这个虚表指针
int a = 0; // 栈中
static int b = 1; // 静态区/数据段
const char* str = "hello world";// 常量区/代码段
int* p = new int[10]; // 堆区
printf("栈:%p\n", &a);
printf("静态区/数据段:%p\n", &b);
printf("静态区/数据段:%p\n", &c);
printf("常量区/代码段:%p\n", str);
printf("堆:%p\n", p);
printf("虚表:%p\n", (*((int*)&b4)));
printf("函数地址:%p\n", &Derive::Func3); // 函数名代表地址 加类域
printf("函数地址:%p\n", &Derive::Func2);
printf("函数地址:%p\n", &Derive::Func1);
return 0;
}
vs 监视窗口看到虚函数表不一定是真实的。可能被处理过
多继承中的虚函数表
class Base1 {
public:
virtual void func1() {
cout << "Base1::func1" << endl;
}
virtual void func2() {
cout << "Base1::func2" << endl;
}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {
cout << "Base2::func1" << endl;
}
virtual void func2() {
cout << "Base2::func2" << endl;
}
private:
int b2;
};
class Derive: public Base1, public Base2 {
public:
virtual void func1() {
cout << "Derive::func1" << endl;
}
virtual void func3() {
cout << "Derive::func3" << endl;
}
private:
int d1;
};
typedef void(*V_FUNC)();
void PrintVFTable(V_FUNC* a)
{
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; ++i) {
printf("[%d]:%p->", i, a[i]);
V_FUNC f = a[i];
f();
}
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
菱形继承、菱形虚拟继承
练习题
-
下面哪种面向对象的方法可以让你变得富有( A )
A. 继承
B. 封装
C. 多态
D. 抽象
-
( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A. 继承
B. 模板
C. 对象的自身引用
D. 动态绑定
-
面向对象设计中的继承和组合,下面说法错误的是?( C )
A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C. 优先使用继承,而不是组合,是面向对象设计的第二原则
D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现 -
以下关于纯虚函数的说法,正确的是( A )
A. 声明纯虚函数的类不能实例化对象
B. 声明纯虚函数的类是虚基类
C. 子类必须实现基类的纯虚函数D. 纯虚函数必须是空函数
-
关于虚函数的描述正确的是( B )
A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B. 内联函数不能是虚函数
C. 派生类必须重新定义基类的虚函数D. 虚函数可以是一个static型的函数
-
关于虚表说法正确的是( D )
A. 一个类只能有一张虚表
B. 基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C. 虚表是在运行期间动态生成的
D. 一个类的不同对象共享该类的虚表 -
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( D )
A. A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B. A类对象和B类对象前4个字节存储的都是虚基表的地址
C. A类对象和B类对象前4个字节存储的虚表地址相同
D. A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表 -
下面程序输出结果是什么? ( A )
#include<iostream> using namespace std; class A{ public: A(char *s) { cout<<s<<endl; } ~A() {} }; class B:virtual public A { public: B(char *s1,char*s2) :A(s1) { cout<<s2<<endl; } }; class C:virtual public A { public: C(char *s1,char*s2) :A(s1) { cout<<s2<<endl; } }; class D:public B, public C { public: D(char *s1,char *s2,char *s3,char *s4) :B(s1,s2) ,C(s1,s3) ,A(s1) { cout<<s4<<endl; } }; int main() { D* p = new D("class A", "class B", "class C", "class D"); delete p; return 0; }
A. class A class B class C class D
B. class D class B class C class A
C. class D class C class B class AD. class A class C class B class D
-
多继承中指针偏移问题?下面说法正确的是( C )
class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive: public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A. $p1 == p2 == p3 $
B. $p1 < p2 < p3 $
C. $ p1 == p3 \ != p2 $
D. $ p1 != p2 != p3 $
-
以下程序输出结果是什么( B )
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl; } virtual void test(){ func(); } }; class B: public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
A. A->0
B. B->1
C. A->1
D. B->0
E. 编译出错
F. 以上都不正确
-
什么是多态?
-
什么是重载、重写(覆盖)、重定义(隐藏)?
-
多态的实现原理?
-
inline
函数可以是虚函数吗? -
静态成员可以是虚函数吗?
-
构造函数可以是虚函数吗?
-
析构函数可以是虚函数吗?
-
对象访问普通函数快还是虚函数更快?
-
虚函数表是在什么阶段生成的,存在哪的?
-
C++菱形继承的问题?虚继承的原理?
-
什么是抽象类?抽象类的作用?
class A
{
public:
virtual void func(int val = 1) {
cout << "A->" << val << endl;
}
virtual void test() {
func(); // 多态调用
}
};
class B: public A
{
// 子类继承重写父类虚函数
// 1. 接口继承。(所以B中func不写virtual 也是虚函数, 符合多态条件 + 缺省也是用的A::func的1)
// 2. 重写的是函数实现。
public:
void func(int val = 0) {
cout << "B->" << val << endl;
}
};
// 多态两个条件:
// 1. 子类虚函数重写的父类虚函数(重写: 三同(函数名 + 参数 + 返回值) + 虚函数)
// 2. 父类指针或者引用去调用虚函数
int main()
{
B* p = new B;
p->test(); // 继承调用 B-> 1
p->func(); // B->0
return 0;
}
类的虚函数,此时子类与基类共用同一张虚表
C. 虚表是在运行期间动态生成的
D. 一个类的不同对象共享该类的虚表
>
>
>
-
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( D )
A. A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B. A类对象和B类对象前4个字节存储的都是虚基表的地址
C. A类对象和B类对象前4个字节存储的虚表地址相同
D. A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表 -
下面程序输出结果是什么? ( A )
#include<iostream> using namespace std; class A{ public: A(char *s) { cout<<s<<endl; } ~A() {} }; class B:virtual public A { public: B(char *s1,char*s2) :A(s1) { cout<<s2<<endl; } }; class C:virtual public A { public: C(char *s1,char*s2) :A(s1) { cout<<s2<<endl; } }; class D:public B, public C { public: D(char *s1,char *s2,char *s3,char *s4) :B(s1,s2) ,C(s1,s3) ,A(s1) { cout<<s4<<endl; } }; int main() { D* p = new D("class A", "class B", "class C", "class D"); delete p; return 0; }
A. class A class B class C class D
B. class D class B class C class A
C. class D class C class B class AD. class A class C class B class D
-
多继承中指针偏移问题?下面说法正确的是( C )
class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive: public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A. $p1 == p2 == p3 $
B. $p1 < p2 < p3 $
C. $ p1 == p3 \ != p2 $
D. $ p1 != p2 != p3 $
-
以下程序输出结果是什么( B )
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl; } virtual void test(){ func(); } }; class B: public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
A. A->0
B. B->1
C. A->1
D. B->0
E. 编译出错
F. 以上都不正确
-
什么是多态?
-
什么是重载、重写(覆盖)、重定义(隐藏)?
-
多态的实现原理?
-
inline
函数可以是虚函数吗? -
静态成员可以是虚函数吗?
-
构造函数可以是虚函数吗?
-
析构函数可以是虚函数吗?
-
对象访问普通函数快还是虚函数更快?
-
虚函数表是在什么阶段生成的,存在哪的?
-
C++菱形继承的问题?虚继承的原理?
-
什么是抽象类?抽象类的作用?
class A
{
public:
virtual void func(int val = 1) {
cout << "A->" << val << endl;
}
virtual void test() {
func(); // 多态调用
}
};
class B: public A
{
// 子类继承重写父类虚函数
// 1. 接口继承。(所以B中func不写virtual 也是虚函数, 符合多态条件 + 缺省也是用的A::func的1)
// 2. 重写的是函数实现。
public:
void func(int val = 0) {
cout << "B->" << val << endl;
}
};
// 多态两个条件:
// 1. 子类虚函数重写的父类虚函数(重写: 三同(函数名 + 参数 + 返回值) + 虚函数)
// 2. 父类指针或者引用去调用虚函数
int main()
{
B* p = new B;
p->test(); // 继承调用 B-> 1
p->func(); // B->0
return 0;
}