1、类的基础知识点
1.1、类和对象
和C中的结构体不同,在C++类中不仅可以定义变量,也可以定义函数。【在C++结构体中也可以定义变量和函数,但是一般情况下都使用类】。
类的成员属性默认都是private;结构体的成员属性默认都是public。
class student
{
public:
// 共有成员(外部接口,可被使用该类的所有代码所使用的)
student();
~student();
void set_age(int age_t);
private:
// 私有成员(只允许本类中的函数访问,而类外部的任何函数都不允许访问)
char name[50];
int age;
protected:
// 保护成员(与private类似,差别表现在继承与派生时)
};
类中的元素称为类的成员;类中的数据称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数。
1.2、类中成员函数定义
(1)声明和定义都放在类体中
// .h文件中
class student
{
public:
// 共有成员(外部接口,可被使用该类的所有代码所使用的)
student();
~student();
void set_age(int age_t)
{
age = age_t;
}
private:
// 私有成员(只允许本类中的函数访问,而类外部的任何函数都不允许访问)
char name[50];
int age;
protected:
// 保护成员(与private类似,差别表现在继承与派生时)
};
(2)声明和定义分离
// .h文件中
class student
{
public:
// 共有成员(外部接口,可被使用该类的所有代码所使用的)
student();
~student();
void set_age(int age_t);
private:
// 私有成员(只允许本类中的函数访问,而类外部的任何函数都不允许访问)
char name[50];
int age;
protected:
// 保护成员(与private类似,差别表现在继承与派生时)
};
// .cpp文件中
void student::set_age(int age_t)
{
age = age_t;
}
1.3、类的作用域
C++类重新定义了一个作用域,类的所有成员都在类的作用域中。在类外使用成员时,需要使用 ::(作用域解析符)来指明成员属于哪个类。
1.4、实例化
用类去创建对象的过程,称为类的实例化。类只声明了类有哪些成员,而并没有为成员分配内存空间。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
类是对象的抽象和概括,而对象是类的具体和实例。
1.5、类对象的存储方式
C++程序占用的内存通常分为四个区:
代码区(code area):存放着程序的二进制代码,由操作系统管理。
全局区(static area):存放全局变量,静态变量,以及常量(字符常量和const修饰的全局变量)。
栈区(stack area):存放所有的局部变量,其空间分配释放由编译器管理,当函数结束,局部变量自动被释放。
堆区(heap area):存放所有动态开辟的变量,其空间分配释放由程序员管理。
类对象存储如下:
在类定义时:
类的成员函数是被放在代码区。
类的静态成员变量是存储在静态区的,即实例化的对象并不包括静态变量的创建。
类的静态成员变量,在类的实例化过程中才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。
#include <iostream>
using namespace std;
class student_1
{
public:
void set_age(int age_t);
private:
char name[50];
int age;
static char aa[8];
}std_1;
class student_2
{
public:
private:
char name[50];
int age;
}std_2;
class student_3
{
}std_3;
int main(int argc, char argv[])
{
cout << " " << sizeof(std_1) << endl;
cout << " " << sizeof(std_2) << endl;
cout << " " << sizeof(std_3) << endl;
return 0;
}
输出:
56
56
1
结论:
一个类的大小,实际就是该类中“成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
注意:
这里的成员变量之和并不是简单的字节数相加,而是还要遵循内存对齐规则。
1.6、this指针
#include <iostream>
using namespace std;
class student
{
public:
void set_id(int grade_id__t, int class_id_t);
void display_id_0()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
void display_id_1()
{
cout << "grade_id: " << this->grade_id << ", class_id: " << this->class_id << endl;
}
private:
char name[50];
int age;
int grade_id;
int class_id;
};
void student::set_id(int grade_id__t, int class_id_t)
{
grade_id = grade_id__t;
class_id = class_id_t;
}
int main(int argc, char argv[])
{
student std_1, std_2;
std_1.set_id(10, 11);
std_2.set_id(101, 34);
std_1.display_id_0();
std_1.display_id_1();
std_2.display_id_0();
std_2.display_id_1();
return 0;
}
结果为:
grade_id: 10, class_id: 11
grade_id: 10, class_id: 11
grade_id: 101, class_id: 34
grade_id: 101, class_id: 34
问题一:
对于上述程序,std_1和std_2各自有不同的内存空间,而成员函数set_id中却并没有指定要对哪一个对象的成员进行操作,那么当std_1调用时函数是如何知道操作的对象是哪一个呢?
答:
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性:
this指针的类型:类的类型* const,this在函数体内不可修改。
只能在“成员函数”的内部使用。
this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
问题二:
this指针的存储位置在哪里?
答:
临时变量都是存储在栈上的,因此this指针作为形参,存储在栈区。
问题三:
this指针可以为空指针吗?
答:
this指针可以为空指针,但切忌通过nullptr去访问指向的数据。
#include <iostream>
using namespace std;
class student
{
public:
void set_id(int grade_id__t, int class_id_t);
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
void display()
{
cout << "grade_id and class_id" << endl;
}
private:
char name[50];
int age;
int grade_id;
int class_id;
};
void student::set_id(int grade_id__t, int class_id_t)
{
grade_id = grade_id__t;
class_id = class_id_t;
}
int main(int argc, char argv[])
{
student *std_1 = nullptr;
std_1->display(); // 执行成功
std_1->display_id(); // 执行失败
return 0;
}
成员函数display()执行成功,而成员函数display_id()执行失败。首先nullptr地址我们是没有访问权限的,非法访问会使程序崩溃。
display()执行成功,因为访问的是成员函数,成员函数不在对象内,就不会对指针解引用,std_1->的作用仅仅是指明了函数的类域,而函数内也没有使用到this指针,所以不会报错。
display_id()执行失败,是因为在打印的时候调用了成员变量grade_id和class_id,而std_1又指向了nullptr,相当于访问了nullptr地址处的数据,因此程序崩溃。
2、类的访问限定符
C++类通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是类外面的代码访问该类中成员的权限。
在类的内部,即类的成员函数中,无论成员被声明为 public、protected还是private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问public的成员,不能访问private、protected属性的成员。
private关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为private。
声明为private的成员和声明为public的成员的次序任意,既可以先出现private部分,也可以先出现public部分。如果既不写private 也不写public,就默认为private。
在一个类体中,private和public可以分别出现多次。每个部分的有效范围到出现另一个访问限定符或类体结束时(最后一个右花括号)为止。
public、private和protected总结如下:
[1]、类的public成员可以被任意实体访问,可以认为它是C语言中的struct结构体,可以直接用a.x这种形式访问;
[2]、类的private成员不能直接被类的实体访问,也不能被子类的实体访问,但是可以被类的成员函数访问;
[3]、类的protected成员不能直接被类的实体访问,但是可以被子类访问,也可以被类的成员函数访问;
#include <iostream>
using namespace std;
class student
{
public:
void set_id(int grade_id__t, int class_id_t);
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
void display()
{
cout << "grade_id and class_id" << endl;
}
int aabb = 103;
private:
char name[50];
int age;
int grade_id;
int class_id;
};
void student::set_id(int grade_id__t, int class_id_t)
{
grade_id = grade_id__t;
class_id = class_id_t;
}
int main(int argc, char argv[])
{
student* std_1 = new student();
std_1->set_id(100, 300);
std_1->display_id();
//std_1->age = 1000; // 本行执行失败,age是私有的变量,不能通过类对象直接访问,可以通过类对象调用类的成员函数去访问。
cout << "aabb: " << std_1->aabb << endl;
return 0;
}
结果为:
grade_id: 100, class_id: 300
aabb: 103
3、类默认的6个成员函数
如果一个类中什么成员都没有,简称为空类。默认成员函数:用户没有显示实现,编译器会默认生成的成员函数称为默认成员函数。那么空类中什么也没有么?其实任何类什么都不写时,编译器会自动生成6个默认成员函数。
C++类中共有6个默认成员函数,分别是构造函数、析构函数、拷贝构造函数、赋值运算符重载、普通对象取地址操作符重载、const对象取地址操作符重载。
3.1、构造函数
类的对象被创建时,编译系统为对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。构造函数的核心作用:初始化对象的数据成员。一个类可以有多个构造函数,为重载关系。
(1)构造函数的特点:
[1]、名字与类名相同,可以有参数,但是不能有返回值(void类型的返回值也不可以)。
[2]、构造函数是在实例化对象时自动执行的,不需要手动调用。
[3]、作用是对对象进行初始化工作,如给成员变量赋值等。
[4]、如果定义类时没有写构造函数,系统会生成一个默认的无参构造函数,默认构造函数没有参数,不做任何工作。
[5]、如果定义了构造函数,系统不再生成默认的无参构造函数。
[6]、对象生成时构造函数自动调用,对象一旦生成,不能在其上再次执行构造函数。
(2)构造函数的分类:
[1]、按参数种类分为:无参构造函数、有参构造函数、有默认参构造函数。
[2]、按类型分为:普通构造函数、拷贝构造函数(赋值构造函数)。
(3)构造函数用法:
/*
无参构造函数
*/
#include <iostream>
using namespace std;
class student
{
public:
// 构造函数,函数名与类名相同
student()
{
grade_id = 5;
class_id = 87;
age = 0;
name = "";
}
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
private:
string name;
int age;
int grade_id;
int class_id;
};
int main(int argc, char argv[])
{
student* std_1 = new student();
std_1->display_id();
return 0;
}
结果为:
grade_id: 5, class_id: 87
/*
有参构造函数,有默认参构造函数
*/
#include <iostream>
using namespace std;
class student
{
public:
// 有默认参构造函数,函数名与类名相同
student(int age_t, int grade_id_t, int class_id_t = 7786);
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
private:
string name;
int age;
int grade_id;
int class_id;
};
student::student(int age_t, int grade_id_t, int class_id_t)
{
age = age_t;
grade_id = grade_id_t;
class_id = class_id_t;
}
int main(int argc, char argv[])
{
student* std_1 = new student(10, 100, 1000);
std_1->display_id();
student* std_2 = new student(10, 100);
std_2->display_id();
return 0;
}
结果为:
grade_id: 100, class_id: 1000
grade_id: 100, class_id: 7786
3.2、析构函数
析构函数的名字在类的名字前加~,没有返回值,但可以被显式的调用,在对象销毁时自动执行,用于进行清理工作,例如释放分配的内存、关闭打开的文件等,这个用途非常重要,可以防止程序员犯错。
析构函数的特点:
[1]、析构函数必须是public属性的。
[2]、析构函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,不管是声明还是定义,函数名前面都不能出现返回值类型,即使是void也不允许。
[3]、析构函数不允许重载的。一个类只能有一个析构函数。
析构函数与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
/*
析构函数
*/
#include <iostream>
using namespace std;
class student
{
public:
// 有默认参构造函数,函数名与类名相同
student(int age_t, int grade_id_t, int class_id_t = 7786);
~student()
{
cout << "the the the xigou" << endl;
}
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
private:
string name;
int age;
int grade_id;
int class_id;
};
student::student(int age_t, int grade_id_t, int class_id_t)
{
age = age_t;
grade_id = grade_id_t;
class_id = class_id_t;
cout << "the the the gouzao" << endl;
}
int main(int argc, char argv[])
{
student* std_1 = new student(10, 100, 1000);
std_1->display_id();
student* std_2 = new student(10, 100);
std_2->display_id();
delete std_1;
delete std_2;
student std_3(101, 102, 103);
std_3.display_id();
return 0;
}
结果为:
the the the gouzao
grade_id: 100, class_id: 1000
the the the gouzao
grade_id: 100, class_id: 7786
the the the xigou
the the the xigou
the the the gouzao
grade_id: 102, class_id: 103
the the the xigou
3.3、拷贝构造函数
拷贝构造函数:
[1]、拷贝构造函数是构造函数的一个重载,一种特殊的构造函数,当对象之间复制时会自动调用拷贝构造函数。
[2]、若没有显式定义拷贝构造函数,编译器会自己生成一个默认拷贝构造,默认的拷贝构造函数对象按内存存储和字节序完成拷贝。
[3]、使用场合:使用旧对象初始化新对象。
[4]、如果自定义了拷贝构造函数,则系统不会默认生成拷贝构造函数了。
/*
拷贝构造函数
*/
#include <iostream>
using namespace std;
class student
{
public:
// 定义一个构造函数
student(int age_t, int grade_id_t, int class_id_t) : age(age_t), grade_id(grade_id_t), class_id(class_id_t) {};
// 定义一个拷贝构造函数
student(const student &p)
{
age = p.age + 100;
grade_id = p.grade_id + 100;
class_id = p.class_id + 100;
}
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
private:
string name;
int age;
int grade_id;
int class_id;
};
int main(int argc, char argv[])
{
student std_1(10, 100, 1000); // std_1是一个student类型的变量
cout << "std_1的大小:" << sizeof(std_1) << endl;
std_1.display_id();
// 基于对象std_1,生成另一个对象std_2
student std_2(std_1);
cout << "std_2的大小:" << sizeof(std_1) << endl;
std_2.display_id();
cout << &std_1 << " " << &std_2 << endl;
return 0;
}
结果为:
std_1的大小:56
grade_id: 100, class_id: 1000
std_2的大小:56
grade_id: 200, class_id: 1100
000000421699FB48 000000421699FB98
3.4、赋值运算符重载
3.4.1、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:
返回值类型 operator操作符(参数列表)
参数列表:
operator函数参数在设置时应当按照操作符操作数的顺序从左向右顺序依次将对应操作数作为参数。
运算符重载的注意事项:
[1]、不能通过连接其他符号来创建新的操作符:比如operator@;
[2]、重载操作符必须有一个类类型参数;
[3]、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义;
[4]、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;
[5]、“.*” “::” “sizeof” “?:” “.”注意以上5个运算符不能重载。
/*
相等运算符 “==” 重载
*/
#include <iostream>
using namespace std;
class student
{
public:
// 定义一个构造函数
student(int age_t, int grade_id_t, int class_id_t) : age(age_t), grade_id(grade_id_t), class_id(class_id_t) {};
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
bool operator==(student p);
private:
string name;
int age;
int grade_id;
int class_id;
};
bool student::operator==(student p)
{
if (grade_id = p.grade_id && class_id == p.class_id)
{
return true;
}
else {
return false;
}
}
int main(int argc, char argv[])
{
student std_1(10, 100, 1000); // std_1是一个student类型的变量
student std_2(1000000, 100, 1000);
std_1.display_id();
std_2.display_id();
if (std_1 == std_2)
{
cout << "is equal." << endl;
}
else {
cout << "is false." << endl;
}
return 0;
}
结果为:
grade_id: 100, class_id: 1000
grade_id: 100, class_id: 1000
is equal.
3.4.2、赋值运算符重载
重载格式:
[1]、参数类型:const T&,传递引用可以提高传参效率;
[2]、返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
[3]、检测是否自己给自己赋值;
[4]、返回*this :要复合连续赋值的含义。
注意:
[1]、赋值运算符只能重载成类的成员函数不能重载成全局函数;
[2]、用户没有显式实现时,编译器会生成一个默认赋值运算符重载,内置类型以值的方式逐字节拷贝。而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值,若自定义类型没有实现对应的赋值运算符重载则也会以值的方式逐字节拷贝。
/*
赋值运算符 “=” 重载
*/
#include <iostream>
using namespace std;
class student
{
public:
// 定义一个构造函数
student(int age_t, int grade_id_t, int class_id_t) : age(age_t), grade_id(grade_id_t), class_id(class_id_t)
{
cout << "gou zao han shu." << endl;
}
~student() {
cout << "xi gou han shu." << endl;
}
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
student& operator=(const student& p)
{
if (this != &p)
{
age = p.age;
grade_id = p.grade_id;
class_id = p.class_id;
}
return *this;
}
public:
string name;
int age;
int grade_id;
int class_id;
};
int main(int argc, char argv[])
{
student std_1(10, 100, 1000); // std_1是一个student类型的变量
student std_2 = std_1; // 调用了 赋值运算符 重载的函数
std_1.display_id();
std_2.display_id();
return 0;
}
结果为:
gou zao han shu.
grade_id: 100, class_id: 1000
grade_id: 100, class_id: 1000
xi gou han shu.
xi gou han shu.
3.5、普通对象取地址操作符重载、const对象取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,因为编译器默认生成的这两个函数就可以满足我们的基本需求。、
/*
取地址运算符重载
*/
#include <iostream>
using namespace std;
class student
{
public:
// 定义一个构造函数
student(int age_t, int grade_id_t, int class_id_t) : age(age_t), grade_id(grade_id_t), class_id(class_id_t)
{
cout << "gou zao han shu." << endl;
}
~student() {
cout << "xi gou han shu." << endl;
}
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
student* operator&() //不写会默认生成
{
return nullptr;
}
const student* operator&()const //不写会默认生成
{
return nullptr;
}
private:
string name;
int age;
int grade_id;
int class_id;
};
int main(int argc, char argv[])
{
student std_1(10, 100, 1000); // std_1是一个student类型的变量
student std_2(1000000, 100, 1000);
std_1.display_id();
std_2.display_id();
return 0;
}
结果为:
gou zao han shu.
gou zao han shu.
grade_id: 100, class_id: 1000
grade_id: 100, class_id: 1000
xi gou han shu.
xi gou han shu.
4、类中默认成员函数的禁用
类在使用中,一般会禁止【拷贝构造函数】、【赋值运算符】。下面给出了一个例子,并给出了什么情况下会调用“拷贝构造函数”,什么情况下会调用“赋值运算符”。
/*
禁止 拷贝构造函数 和 赋值运算符
*/
#include <iostream>
using namespace std;
class student
{
public:
// 定义一个无参构造函数
student() {};
// 定义一个有参构造函数
student(int age_t, int grade_id_t, int class_id_t) : age(age_t), grade_id(grade_id_t), class_id(class_id_t)
{
cout << "gou zao han shu." << endl;
}
~student() {
cout << "xi gou han shu." << endl;
}
// 禁止 拷贝构造函数
student(const student&) = delete;
// 禁止 赋值运算符
student& operator=(const student& ) = delete;
void display_id()
{
cout << "grade_id: " << grade_id << ", class_id: " << class_id << endl;
}
public:
string name;
int age;
int grade_id;
int class_id;
};
int main(int argc, char argv[])
{
student std_1(10, 100, 1000); // std_1是一个student类型的变量
std_1.display_id();
// 调用了 拷贝构造函数;但是由于禁止了拷贝构造函数的操作,所以运行失败
//student std_2 = std_1; // 对应于代码【student(const student&) = delete;】
// 调用了 拷贝构造函数;但是由于禁止了拷贝构造函数的操作,所以运行失败
//student std_3(std_1); // 对应于代码【student(const student&) = delete;】
// 调用了 赋值运算符;但是由于禁止了赋值运算符的操作,所以运行失败
student std_4;
//std_4 = std_1; // 对应于代码【student& operator=(const student& ) = delete;】
return 0;
}
结果为:
gou zao han shu.
grade_id: 100, class_id: 1000
xi gou han shu.
xi gou han shu.
5、面向对象的三大特征
C++三大特性:封装、继承、多态:
[1]、封装:实现代码模块化。【作用:代码模块化】
[2]、继承:实现代码扩展。【作用:代码重用】
[3]、多态:分为静态多态和动态多态。【作用:接口重用】
[3.1]、静态多态:函数重载和泛型编程。
[3.2]、动态多态:虚函数重写。
6、封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作。使用封装有四大好处:1、良好的封装能够减少耦合;2、类内部的结构可以自由修改;3、可以对成员进行更精确的控制;4、隐藏信息,实现细节。
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
封装类的访问类型:
私有成员(private):只有此类的成员函数才能访问。
保护成员(protected):类和派生类的成员函数都能访问,但类外不可以访问。
公有成员(public):可以在类外访问。
特点:
隔离变化,提高复用性,提高安全性。
缺点:
封装太多,影响运行效率。使用者不知道代码的具体实现。
示例:计算一个长方体的体积,及相关操作。
/*
封装
*/
#include<iostream>
#include<string>
using namespace std;
class Cube
{
public:
//长的设置
void set_L(int L)
{
m_L = L;
}
int get_L()
{
return m_L;
}
//宽的设置
void set_W(int W)
{
m_W = W;
}
int get_W()
{
return m_W;
}
//高的设置
void set_H(int H)
{
m_H = H;
}
int get_H()
{
return m_H;
}
//计算体积
int calculate_V()
{
int V;
V = m_L * m_W * m_H;
return V;
}
//利用成员函数来判断两个立方体是否相等
bool isSame(Cube& c)
{
if (m_L == c.get_L() && m_W == c.get_W() && m_H == c.get_H())
{
return true;
}
return false;
}
private:
int m_L;
int m_W;
int m_H;
};
//利用全局函数来判断两个立方体是否相等
bool isSame(Cube& c1, Cube& c2)
{
if (c1.get_L() == c2.get_L() && c1.get_W() == c2.get_W() && c1.get_H() == c2.get_H())
{
return true;
}
return false;
}
int main(int argc, char argv[])
{
Cube c1;
c1.set_L(10);
c1.set_W(10);
c1.set_H(10);
cout << "C1体积为:" << c1.calculate_V() << endl;
Cube c2;
c2.set_L(4);
c2.set_W(25);
c2.set_H(10);
cout << "C2体积为:" << c2.calculate_V() << endl << endl;
bool ret1 = isSame(c1, c2);
cout << "全局函数判断结果:" << endl;
if (ret1)
{
cout << "C1与C2相等" << endl;
}
else
{
cout << "C1与C2不相等" << endl;
}
cout << endl << endl;
bool ret2 = c1.isSame(c2);
cout << "成员函数判断结果:" << endl;
if (ret2)
{
cout << "C1与C2相等" << endl;
}
else
{
cout << "C1与C2不相等" << endl;
}
system("pause");
return 0;
}
结果为:
C1体积为:1000
C2体积为:1000
全局函数判断结果:
C1与C2不相等
成员函数判断结果:
C1与C2不相等
7、继承
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类的复用。
示例:举例说明继承的一段代码。person是父类,student是派生类。
/*
继承的一个展示
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
void print()
{
cout << name << " " << id << endl;
}
protected:
string name = "hyx";
int id = 2023;
};
//Student 继承了person类,
// 继承后父类Person的成员(成员函数+成员变量)都会变成子类的一部分。
// 相当于是子类包括父类所有的(成员+函数)和它原来本身所有的(成员+函数).
class student : public person
{
protected:
int stuid;
};
int main(int argc, char argv[])
{
student std_1;
std_1.print();
return 0;
}
结果为:
hyx 2023
7.1、知识点
定义格式:
继承关系:
[1]、public继承;[2]、protected继承;[3]、private继承。
访问限定符:
[1]、public访问;[2]、protected访问;[3]、private访问。
访问限定符中:public成员可以被外部访问、继承访问。protected成员,不可以被外部访问,但可以被继承访问。private成员,外部和继承都不可以被访问。
子类继承基类,基类的成员在子类是否可以被访问:
类成员/ 继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结如下:
[1]、基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
[2]、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符(protected)是因继承才出现的。
[3]、实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式==Min(成员在基类的访问限定符,继承方式),public > protected > private。
[4]、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
[5]、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
7.2、基类和派生类对象赋值转化
“派生类对象”可以赋值给“基类的对象 / 基类的指针 / 基类的引用”。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
“基类对象”不能赋值给“派生类对象”。
/*
子类和父类 赋值关系
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
void print()
{
cout << name << " " << id << endl;
}
protected:
string name = "hyx";
int id = 2023;
};
//Student 继承了person类,
// 继承后父类Person的成员(成员函数+成员变量)都会变成子类的一部分。
// 相当于是子类包括父类所有的(成员+函数)和它原来本身所有的(成员+函数).
class student : public person
{
public:
protected:
int stuid;
};
int main(int argc, char argv[])
{
student std_1;
person p_1;
p_1 = std_1;
p_1.print();
//std_1 = p_1; // 出错
return 0;
}
结果为:
hyx 2023
7.3、继承中的作用域
作用域:
[1]、在继承体系中基类和派生类都有独立的作用域。
[2]、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,如果想使用同名成员中的基类,可以使用“基类::基类成员”显示访问)
[3]、需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
[4]、注意在实际中在继承体系里面最好不要定义同名的成员。
/*
子类和父类的 作用域
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
person()
{
cout << "person gouzao" << endl;
}
~person()
{
cout << "person xigou" << endl;
}
protected:
string _name = "小郝"; // 姓名
int _num = 111; // 身份证号
};
//Student类与Person类中都有_num,但是子类(Student类)会把父类中的_num隐藏,此时再访问子类中的_num,便是子类的_num=999
class student : public person
{
public:
student()
{
cout << "student gouzao" << endl;
}
~student()
{
cout << "student xigou" << endl;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl;
cout << " 身份证号:" << person::_num << endl;
}
protected:
int _num = 999; // 学号
};
int main(int argc, char argv[])
{
student std_1;
std_1.Print();
return 0;
}
结果为:
person gouzao
student gouzao
姓名:小郝
学号:999
身份证号:111
student xigou
person xigou
7.4、派生类的默认成员函数
我们知道普通的类有6个默认成员函数,但是如果我们继承了某个类之后,这个子类的6个默认成员函数会做什么呢?
“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员又分别有什么作用呢?
7.4.1、派生类的构造函数
在派生类的构造函数中,一共执行两个行为:[1]、派生类自己的成员,调用自己的构造函数初始化;[2]、继承的父类成员,必须调用父类的构造函数初始化。顺序是:先调用父类构造函数,再调用子类的构造函数。
/*
子类和父类的 作用域
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
person()
{
cout << "person gouzao" << endl;
}
~person()
{
cout << "person xigou" << endl;
}
protected:
string _name = "小郝"; // 姓名
int _num = 111; // 身份证号
};
//Student类与Person类中都有_num,但是子类(Student类)会把父类中的_num隐藏,此时再访问子类中的_num,便是子类的_num=999
class student : public person
{
public:
student()
{
cout << "student gouzao" << endl;
}
~student()
{
cout << "student xigou" << endl;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl;
cout << " 身份证号:" << person::_num << endl;
}
protected:
int _num = 999; // 学号
};
int main(int argc, char argv[])
{
student std_1;
std_1.Print();
return 0;
}
结果为:
person gouzao
student gouzao
姓名:小郝
学号:999
身份证号:111
student xigou
person xigou
7.4.2、派生类的拷贝构造函数
在派生类的拷贝构造函数中,也需要执行两个行为:[1]、对于子类自己的成员,直接调用自己的拷贝构造(内置类型拷贝,自定义类型调用它的拷贝构造);[2]、对于继承的父类成员,将调用父类的拷贝构造函数初始化。
示例:
父类中定义拷贝构造函数,字类中不做处理,则赋值时会调用到父类的拷贝构造函数。
如果子类中也定义拷贝构造函数,则会调用子类的拷贝构造函数和父类的构造函数。
/*
子类和父类的 作用域
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
person()
{
cout << "person gouzao" << endl;
}
//定义一个拷贝构造函数
person(const person& p)
{
_name = p._name;
_num = p._num;
cout << "person gouzao 2" << endl;
}
~person()
{
cout << "person xigou" << endl;
}
protected:
string _name = "小郝"; // 姓名
int _num = 111; // 身份证号
};
//Student类与Person类中都有_num,但是子类(Student类)会把父类中的_num隐藏,此时再访问子类中的_num,便是子类的_num=999
class student : public person
{
public:
student()
{
cout << "student gouzao" << endl;
}
~student()
{
cout << "student xigou" << endl;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl;
cout << " 身份证号:" << person::_num << endl;
}
protected:
int _num = 999; // 学号
};
int main(int argc, char argv[])
{
student std_1;
cout << "-----------------------------" << endl;
student std_2 = std_1;
std_2.Print();
return 0;
}
结果为:
person gouzao
student gouzao
-----------------------------
person gouzao 2
姓名:小郝
学号:999
身份证号:111
student xigou
person xigou
student xigou
person xigou
7.4.3、派生类中的operator=赋值运算符重载
和上面的几乎完全一致。在派生类中使用“=”运算符,也会执行两个行为:[1]、子类调用自身的“=”运算符重载;[2]、自动调用父类的"="的运算符重载。一般对内置类型不用处理,对于自定义类型需要。当显式调用的时候,一定要加上上父类的类域,因为子类的“=”运算符重载会将父类的“=”运算符隐藏,进而无限递归。
/*
派生类的 赋值运算符重载
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
person()
{
cout << "person gouzao" << endl;
}
~person()
{
cout << "person xigou" << endl;
}
//定义一个赋值构造函数
person& operator=(const person& p)
{
_name = p._name;
_num = p._num;
cout << "person = " << endl;
return *this;
}
protected:
string _name = "小郝"; // 姓名
int _num = 111; // 身份证号
};
//Student类与Person类中都有_num,但是子类(Student类)会把父类中的_num隐藏,此时再访问子类中的_num,便是子类的_num=999
class student : public person
{
public:
student()
{
cout << "student gouzao" << endl;
}
~student()
{
cout << "student xigou" << endl;
}
//定义一个赋值构造函数
student& operator=(const student& s)
{
_age = s._age;
cout << "student = " << endl;
return *this;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 年龄:" << _age << endl;
cout << " 身份证号:" << person::_num << endl;
}
protected:
int _age = 999;
};
int main(int argc, char argv[])
{
student std_1;
cout << "-------------" << endl;
student std_2;
cout << "-------------" << endl;
std_2 = std_1;
std_2.Print();
return 0;
}
结果为:
person gouzao
student gouzao
-------------
person gouzao
student gouzao
-------------
student =
姓名:小郝
年龄:999
身份证号:111
student xigou
person xigou
student xigou
person xigou
7.4.5、派生类中的析构函数
规则同上,但顺序不同:[1]、先调用派生类的析构函数;[2]、再调用基类的析构函数。
/*
子类和父类的 作用域
*/
#include<iostream>
#include<string>
using namespace std;
//定义一个person类
class person
{
public:
person()
{
cout << "person gouzao" << endl;
}
~person()
{
cout << "person xigou" << endl;
}
protected:
string _name = "小郝"; // 姓名
int _num = 111; // 身份证号
};
//Student类与Person类中都有_num,但是子类(Student类)会把父类中的_num隐藏,此时再访问子类中的_num,便是子类的_num=999
class student : public person
{
public:
student()
{
cout << "student gouzao" << endl;
}
~student()
{
cout << "student xigou" << endl;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl;
cout << " 身份证号:" << person::_num << endl;
}
protected:
int _num = 999; // 学号
};
int main(int argc, char argv[])
{
student std_1;
std_1.Print();
return 0;
}
结果为:
person gouzao
student gouzao
姓名:小郝
学号:999
身份证号:111
student xigou
person xigou
7.5、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
/*
父类的友元函数,是不能访问子类的成员的
*/
#include<iostream>
#include<string>
using namespace std;
class student;
class person
{
public:
friend void Display(const person& p, const student& s);
protected:
string _name; // 姓名
};
class student : public person
{
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);
}
可以看到虽然子类继承了父类,而父类的友元函数Display只能访问父类的成员,而子类的却不可以被访问(但是子类依然可以调用父类的成员)。
7.6、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
/*
父类中的静态变量
*/
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person() {}
protected:
string _name; // 姓名
public:
int _count = 0; //普通count变量
static int _staticcount; // 静态成员count
};
int person::_staticcount = 0;
class student : public person
{
protected:
int _stuNum; // 学号
};
class graduate : public student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
person p;
student s;
graduate g;
p._count++;
p._staticcount++;
s._count++;
s._staticcount++;
g._count++;
g._staticcount++;
cout << "Person: 普通count:" << p._count << " " << "Person: 静态count:" << p._staticcount << endl;
cout << "Student: 普通count:" << s._count << " " << "Student: 静态count:" << s._staticcount << endl;
cout << "Graduate: 普通count:" << g._count << " " << "Geaduate: 静态count:" << g._staticcount << endl;
return 0;
}
结果为:
Person: 普通count:1 Person: 静态count:3
Student: 普通count:1 Student: 静态count:3
Graduate: 普通count:1 Geaduate: 静态count:3
7.7、单继承与多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
7.8、菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承存在的问题:数据冗余和二义性。
数据冗余:
student类和teacher类都继承了person类的成员,assistant类再继承student和teacher,则在assistant类中会有两份person的成员,调用两次构造函数,造成数据冗余。
解决数据冗余:
菱形虚拟继承,即用关键字virtual来修饰多继承中被继承的父类。
class assistant :virtual public student, virtual public teacher
二义性:
当assistant的对象调用person中的对象时,编译器不知道调用的是student和teacher中哪一个类中的。
解决二义性:
assistant a;
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.student::name = "xxx";
a.teacher::name = "yyy";
7.9、问答
7.9.1、如何设计一个不能被继承的类
答:将父类的构造函数私有化(private),这样在子类中是不可见的。当子类对象实例化时,会无法调用构造函数。
但是在C++11中,增添了final关键字,可以将当前类设置为不可被继承,无论是否实例化,一旦有继承它的会立即报错。
class student final
{
private:
int age = 0;
}
7.9.2、多继承中指针偏移问题,根据这段代码,正确选项是( C )
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
答:这段程序首先是两个类,分别为Base1和Base2。然后Derive类继承了Base1和Base2类(多继承)。然后程序是创建一个Derive类对象,然后取其地址分别赋为Base1类型的指针,Base2类型的指针,和Derive本身类型。
在这我们用图来解释一下:
这里还是需要特殊说明一下:在多继承中,在内存中的位置先后取决于继承时的先后顺序。
例如:将
class Derive : public Base1, public Base2 { public: int _d; };
改为
class Derive : public Base2, public Base1 { public: int _d; };
则Base2则会在Base1上面,答案就会变为p2==p3!=p1。
但这只是指针的比较,类对象中的存储位置地址还是先申请的地址低,后申请的地址高。即在地址中的大小_d > _b2 > _b1。
8、多态
面向对象的三大特征:封装,继承,多态。封装:本质是对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承:本质是为了复用,复用基类的数据成员和方法。多态:多态的实现要求必须是公有继承作为前提。
多态的概念通俗来讲:是因为有了虚函数,通过继承,派生类可以重写虚函数,才有了多态。
多态就是当要完成某个行为时,不同的对象去完成会产生不同的效果,或者不同的对象处理某一件事有不同的方法。例如:在火车站买票,普通成年人,需要全价买票,学生可以半价买票,军人可以优先买票。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
继承中构成多态的两个条件:
[1]、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
[2]、必须通过基类的指针或者引用调用虚函数。
一个示例如下:
/*
多态
*/
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
// 定义一个虚函数
virtual void say()
{
cout << "say Animal." << endl;
}
private:
};
class Dog : public Animal
{
public:
// 重写父类虚函数
virtual void say()
{
cout << "say Dog." << endl;
}
private:
};
class Cat : public Animal
{
public:
// 重写父类虚函数
virtual void say()
{
cout << "say Cat." << endl;
}
private:
};
void test(Animal& A)
{
// [1] 必须通过基类的指针或者引用调用虚函数
// [2] 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
A.say();
}
int main()
{
Animal a;
Dog d;
Cat c;
test(a);
test(d);
test(c);
return 0;
}
结果为:
say Animal.
say Dog.
say Cat.
8.1、虚函数
即被virtual修饰的类成员函数称为虚函数。
class Animal
{
public:
// 定义一个虚函数
virtual void say()
{
cout << "say Animal." << endl;
}
private:
};
8.2、虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Dog : public Animal
{
public:
// 重写父类虚函数
virtual void say()
{
cout << "say Dog." << endl;
}
private:
};
class Cat : public Animal
{
public:
// 重写父类虚函数
virtual void say()
{
cout << "say Cat." << endl;
}
private:
};
8.3、C++11中的override和final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能再被重写。
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
8.4、纯虚函数
在虚函数的后面写上=0 ,则这个函数为纯虚函数。
class Animal
{
public:
// 定义一个纯虚函数
virtual void say() { } = 0;
private:
};
8.5、抽象类
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
/*
多态
*/
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
// 定义一个纯虚函数
virtual void say() = 0;
private:
};
class Dog : public Animal
{
public:
// 重写父类纯虚函数
virtual void say()
{
cout << "say Dog." << endl;
}
private:
};
class Cat : public Animal
{
public:
// 重写父类纯虚函数
virtual void say()
{
cout << "say Cat." << endl;
}
private:
};
void test(Animal& A)
{
// [1] 必须通过基类的指针或者引用调用虚函数
// [2] 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
A.say();
}
int main()
{
//Animal a; // 抽象类不能实例化对象
Dog d;
Cat c;
//test(a);
test(d);
test(c);
return 0;
}
结果为:
say Dog.
say Cat.
8.6、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
8.7、虚函数表
虚函数表实际是一个函数指针数组,虚函数表简称虚表。虚函数表里面保存的都是虚函数的地址。
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
// 定义一个虚函数
virtual void say()
{
cout << "say Animal." << endl;
}
private:
int _b;
char _c;
};
void test(Animal& A)
{
// [1] 必须通过基类的指针或者引用调用虚函数
// [2] 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
A.say();
}
int main()
{
Animal a;
test(a);
return 0;
}
结果为:
say Animal.
分析:
通过观察测试我们发现Base对象是16bytes,除了_b,_c成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
8.8、派生类的虚函数表
派生类会继承基类的虚函数,会继承基类的虚表。但是派生类和基类的_vfptr变量内容不相等,说明两个虚表不是同一种虚表,只是虚表里的内容相同,所以会调用同一个函数。
派生类重写基类虚函数,会重写派生类虚函数表里的内容,将对应位置覆盖成重写虚函数的指针。
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
// 定义一个虚函数
virtual void say()
{
cout << "say Animal." << endl;
}
private:
int _b;
char _c;
};
class Dog : public Animal
{
public:
// 重写父类虚函数
virtual void say()
{
cout << "say Dog." << endl;
}
private:
};
void test(Animal& A)
{
// [1] 必须通过基类的指针或者引用调用虚函数
// [2] 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
A.say();
}
int main()
{
Animal a;
Dog d;
test(a);
test(d);
return 0;
}
得知:
[1]、派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
[2]、基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1。
8.9、参考
9、问答
9.1、类、结构体 内存对齐规则
类和结构体的对齐规则:
【1】第一个成员在与 结构体/类 偏移量为0的地址处。
【2】其他成员变量要对齐到有效对齐数的整数倍的地址处。
有效对齐数 = 编译器默认的一个对齐数 与 该成员所占字节数的较小值。
64位操作系统默认的对齐数为8,32位的为4。
【3】结构体总大小为:最大对齐数(所有变量类型所占字节数最大者 与 默认对齐参数 取最小)的整数倍。
【4】如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数(该结构体内所有变量类型所占字节数最大者与默认对齐参数取最小)的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base gouzao" << endl; };
~Base() { cout << "Base xigou" << endl; };
void print()
{
cout << "print print print : " << _aa << endl;
}
static void set_aa()
{
_aa += 1;
}
private:
static int _aa;
int _bb;
char _cc[5];
};
int main()
{
// 在监控类Base的对象时,发现没有_aa的内存(静态成员变量),表明静态成员变量不是存储在对象中,而是存储在静态全局中。
// 也没有成员函数print()、静态成员函数set_bb()的内存,表明成员函数不是存储在对象中,而是存储在公共代码块中。
Base b;
cout << "=============== " << sizeof(b) << endl;
return 0;
}
结果为:
Base gouzao
=============== 12
Base xigou
// int _bb, 占4字节
// char _cc[5], 占5字节
// 系统为64位,默认为8字节对齐
// 有效对齐数:4
// 最大对齐数:min(4(类中最大成员类型为int,占4字节), 8(默认对齐数)) = 4
// Base类对象b,一共包含上述两种,共9字节,
// 最终占字节数 % 4 == 0
// 所以最终一共占12字节
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base gouzao" << endl; };
~Base() { cout << "Base xigou" << endl; };
void print()
{
cout << "print print print : " << _aa << endl;
}
static void set_aa()
{
_aa += 1;
}
virtual void set_bb() {};
// virtual和static不能一起使用
// 仅非静态成员函数可以使虚函数
// static virtual void set_bb_1() {}; // 出错
private:
static int _aa;
int _bb;
char _cc[5];
};
int main()
{
// 在监控类Base的对象时,发现没有_aa的内存(静态成员变量),表明静态成员变量不是存储在对象中,而是存储在静态全局中。
// 也没有成员函数print()、静态成员函数set_bb()的内存,表明成员函数不是存储在对象中,而是存储在公共代码块中。
Base b;
cout << "=============== " << sizeof(b) << endl;
return 0;
}
结果为:
Base gouzao
=============== 24
Base xigou
// 虚函数表, 占8字节
// int _bb, 占4字节
// char _cc[5], 占5字节
// 系统为64位,默认为8字节对齐
// 有效对齐数:8
// 最大对齐数:min(8(类中最大成员类型为虚函数表,占8字节), 8(默认对齐数)) = 8
// Base类对象b,一共包含上述三种,共17字节,
// 最终占字节数 % 8 == 0
// 所以最终一共占24字节
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base gouzao" << endl; };
~Base() { cout << "Base xigou" << endl; };
void print()
{
cout << "print print print : " << _aa << endl;
}
static void set_aa()
{
_aa += 1;
}
private:
static int _aa;
int _bb;
double _gh;
char _cc[5];
};
int main()
{
// 在监控类Base的对象时,发现没有_aa的内存(静态成员变量),表明静态成员变量不是存储在对象中,而是存储在静态全局中。
// 也没有成员函数print()、静态成员函数set_bb()的内存,表明成员函数不是存储在对象中,而是存储在公共代码块中。
Base b;
cout << "=============== " << sizeof(double) << endl;
return 0;
}
结果为:
Base gouzao
=============== 24
Base xigou
// int _bb, 占4字节
// double _bb, 占8字节
// char _cc[5], 占5字节
// 系统为64位,默认为8字节对齐
// 有效对齐数:4
// 最大对齐数:min(8(类中最大成员类型为double,占8字节), 8(默认对齐数)) = 8
// Base类对象b,一共包含上述三种,共17字节,
// 最终占字节数 % 8 == 0
// 所以最终一共占24字节
9.2、this指针
分析下面的实例:
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base gouzao" << endl;
}
~Base() { cout << "Base xigou" << endl; };
void set(int a, int b)
{
_aa = a;
_bb = b;
}
void print()
{
cout << "this: " << this << endl;
cout << "print: " << _aa << " " << _bb << endl;
cout << "print: " << this->_aa << " " << this->_bb << endl;
}
void print_1()
{
cout << "==========" << endl;
}
private:
int _aa;
int _bb;
};
int main()
{
Base b1;
Base b2;
b1.set(11, 22);
b2.set(41, 56);
b1.print();
b2.print();
cout << "b1: " << &b1 << endl;
cout << "b2: " << &b2 << endl;
Base b3 = nullptr;
// 对象为空指针,但是print_1()函数中没有使用到this指针,成员函数存放在公共代码区,不存储在对象中,所以这样调用没有问题。
// 如果函数中访问了this指针,就会出错。
b3.print_1();
return 0;
}
结果为:
Base gouzao
Base gouzao
this: 0x7ffc03564958
print: 11 22
print: 11 22
this: 0x7ffc03564950
print: 41 56
print: 41 56
b1: 0x7ffc03564958
b2: 0x7ffc03564950
==========
Base xigou
Base xigou
Base xigou
// 打印this指针的值,发现和对象的地址是一样的。
b1和b2打印结果的时候调用的print()是同一个函数, 那么编译器如何知道b1是11 22,b2是41 56呢?在print()函数中访问的变量都是_aa,_bb,而且print()函数存放在公共代码区,是如何访问的?
因为C++增加了一个this指针,print()会增加一个this形参。C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base gouzao" << endl;
}
~Base() { cout << "Base xigou" << endl; };
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
void set(Base* this, int a, int b)
{
this->_aa = a;
this->_bb = b;
}
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
void print(Base* this)
{
cout << "this: " << this << endl;
cout << "print: " << tis->_aa << " " << this->_bb << endl;
}
private:
int _aa;
int _bb;
};
int main()
{
Base b1;
Base b2;
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
b1.set(&b1, 11, 22);
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
b2.set(&b2, 41, 56);
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
b1.print(&b1);
// 不能显示的写出来,因为this是隐含的,可以直接在类里面用
b2.print(&b2);
cout << "b1: " << &b1 << endl;
cout << "b2: " << &b2 << endl;
return 0;
}
this指针的特性总结:
[1]、this指针的类型:类类型* const。
[2]、只能在“成员函数”的内部使用。
[3]、this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
[4]、this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
参考:[ C++ ] 一篇带你了解C++中隐藏的this指针_c++ this-CSDN博客
9.3、
End、结语
感谢参考的文档;参考的文档太多,不做一一列举。
大家也可以提出一些问题,我也会为大家在问答中更新;