讨论C++类与对象
- C语言结构体和C++类的对比
- 类的实例化
- 类对象的大小
- 猜想一
- 猜想二
- 针对上述猜想的实践
- `this`指针
- 不同对象调用成员函数
- 类的6个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 浅拷贝和深拷贝
- 赋值运算符重载
- 初始化列表
- 初始化顺序
C语言结构体和C++类的对比
在C语言中,要想描述一个复杂结构,需要使用结构体。例如描述一个学生,需要有学号,学生姓名,年龄以及性别。
上述代码结果:
C语言中的结构体可以描述复杂结构,但想要修改就需要在外部定义函数,传入结构体指针,较为复杂。
void modifyStudent(struct Student* pst) {printf("请输入想要修改的年龄:");int age = 0;scanf("%d", &age);pst->age = age; }
在C++中,引入了类的概念,它类似于C语言的结构体,可以描述一个复杂结构,不同于结构体,类中是可以定义成员函数的,可以极大方便对成员变量进行修改。
为了兼容C语言,C++中使用
struct
关键字可以定义类,也可以使用class
关键字定义类。
struct
关键字定义的类没有访问修饰限定符,默认都是public
公开访问的。
class
关键字定义的类可以自定访问修饰限定符,private
、protected
、public
,默认的访问权限是private
私有的。类外不能访问。
class Student {
public:int _stuId;std::string _name;int _age;std::string _gender;
};
如上代码便定义出一个学生类,由于成员变量都是public
公有的,所以在类外也可以访问。
类的实例化
用类类型创建对象的过程,称为类的实例化。
int main() {Student st1;st1._stuId = 101;st1._name = "张三";st1._age = 23;st1._gender = "男";std::cout << "学生> 学号:" << st1._stuId << ", 姓名:" << st1._name <<", 年龄:" << st1._age << ", 性别:" << st1._gender << std::endl;return 0;
}
- 如果成员变量使用
private
修饰,类外就不能直接进行访问,会报错。
类对象的大小
类中既有成员变量,又有成员函数,那么类对象的存储方式是什么样的呢?
猜想一
成员变量和成员函数都存储在类对象中。
class Test1 {
public:void test() {}
private:int _a;
};
猜想二
成员变量存储在类对象中,成员函数存储在公共代码段中,由函数表记录。
针对上述猜想的实践
- 使用
sizeof
计算类对象的大小。如果成员函数存储在类对象中,则对象所占内存大小一定大于int
所占内存大小。
int main() {Test1 t1;std::cout << sizeof(t1) << std::endl;return 0;
}
由此可得出结论:只有成员变量存储在类对象中,成员函数存储在公共代码段中。
类对象所占空间大小也遵循内存对齐规则。
this
指针
针对上述内容的讨论,可以得知每个实例化的对象都有一份自己的成员变量,但成员函数存放在公共代码段中,是所有对象共有的,但通过代码可知,通过不同对象调用成员函数,得到的成员变量是不同的。
不同对象调用成员函数
class Student {
public:void print() {std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<", 年龄:" << _age << ", 性别:" << _gender << std::endl;}public:int _stuId;std::string _name;int _age;std::string _gender;
};int main() {Student st1;st1._stuId = 101;st1._name = "张三";st1._age = 23;st1._gender = "男";st1.print();Student st2;st2._stuId = 102;st2._name = "李四";st2._age = 24;st2._gender = "女";st2.print();return 0;
}
- 由此可见,在成员函数中一定有一个标识,表示不同的对象调用,这个标识就是***
this
指针*。
在每一个成员函数中,都有一个隐藏的参数,即this
指针,该指针表示当前调用的对象。
类似于如上图所示,不过这个this
指针不需要我们写出来,是编译器默认生成的,这就是即使成员函数不存放在对象中,不同的对象调用成员函数所显示的值会不一样了。
类的6个默认成员函数
默认成员函数即我们不写,编译器会自动生成的成员函数。
构造函数
对于学生类,成员变量如果设为私有,类外便不能访问,实例化对象时,就不能直接赋值。若提供对外的
getter & setter
接口,也只能在实例化之后再一一赋值。如何达到实例化对象中就完成对对象成员变量的初始化,这就需要构造函数来完成。构造函数是一个特殊的成员函数,函数名和类名相同,没有返回值,实例化对象时由编译器自动调用,且在对象的生命周期中只调用一次。
class Student {
public:Student() {std::cout << "无参的构造函数调用了" << std::endl;} // 无参的构造函数Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数std::cout << "有参的构造函数调用了" << std::endl;_stuId = stuId;_name = name;_age = age;_gender = gender;}void print() {std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<", 年龄:" << _age << ", 性别:" << _gender << std::endl;}private:int _stuId;std::string _name;int _age;std::string _gender;
};int main() {Student st1;st1.print();Student st2(102, "李四", 24, "女");st2.print();return 0;
}
C++标准规定,若不对成员变量做初始化,编译器默认对内置类型不做处理,自定义类型会调用其默认的构造函数。
当然,如果我们不显示写构造函数,编译器会生成不带参数的构造函数,如果我们显示写了,编译器就不再生成不带参数的构造函数了。
class Student {
public:
// Student() {
// std::cout << "无参的构造函数调用了" << std::endl;
// } // 无参的构造函数Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数std::cout << "有参的构造函数调用了" << std::endl;_stuId = stuId;_name = name;_age = age;_gender = gender;}void print() {std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<", 年龄:" << _age << ", 性别:" << _gender << std::endl;}private:int _stuId;std::string _name;int _age;std::string _gender;
};int main() {Student st1; // 此处会报错,因为显示写了带参数的构造函数,编译器不再生成不带参的构造函数。st1.print();Student st2(102, "李四", 24, "女");st2.print();return 0;
}
为了省事,不写那么多构造函数,我们也可以采用参数全缺省的形式来充当默认成员函数。
class Student {
public:Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数std::cout << "有参全缺省的构造函数调用了" << std::endl;_stuId = stuId;_name = name;_age = age;_gender = gender;}void print() {std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<", 年龄:" << _age << ", 性别:" << _gender << std::endl;}private:int _stuId;std::string _name;int _age;std::string _gender;
};
实例化对象时,若给初始值,则直接采用缺省值。
析构函数
析构函数与构造函数的作用刚好相反,对象在销毁时自动调用析构函数,完成对象中资源的清理工作。
- 语法:
- 析构函数名与类名相同,在函数名前加
~
。 - 无返回值。
- 一个类只能有一个析构函数,析构函数不能重载。
- 对象生命周期结束时,编译器自动调用析构函数。
- 析构函数名与类名相同,在函数名前加
class Student {
public:Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数_stuId = stuId;_name = name;_age = age;_gender = gender;}void print() {std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<", 年龄:" << _age << ", 性别:" << _gender << std::endl;}~Student() {std::cout << "Student类的析构函数调用了" << std::endl;}private:int _stuId;std::string _name;int _age;std::string _gender;
};int main() {Student st1(102, "李四", 24, "女");st1.print();return 0;
}
析构函数也是特殊的成员函数,因此对内置类型不做处理,自定义类型调用其析构函数。
拷贝构造函数
拷贝构造函数是构造函数的一个重载,参数只能有一个,且是类类型的引用。
int main() {Student st1(102, "李四", 24, "女");st1.print();Student st2(st1); // 使用拷贝构造函数实例化对象st2.print();return 0;
}
若未显示定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数。对象会按照内存存储字节序完成拷贝,即浅拷贝。
浅拷贝和深拷贝
- 如上述所说,浅拷贝只是对内存的直接复制,如果只是栈上开辟空间的变量,影响还没有那么大,但如果是在堆上开辟的空间,那么只拷贝值会影响非常大。
class Test {
public:Test() {_a = (int*)malloc(10 * sizeof(int));b = 20;}~Test() {free(_a);_a = nullptr;}
private:int* _a;int b;
};
- 因此,面对这种情形,默认生成的拷贝构造函数就不能满足条件了,因此就需要自己显示定义拷贝构造函数,来达到深拷贝。
赋值运算符重载
C++为了增强代码的可读性,引入了运算符重载。
- 语法:
- 返回值类型
operator=
=(参数列表);
class Test {
public:Test() {_a = (int*)malloc(10 * sizeof(int));b = 20;}~Test() {free(_a);_a = nullptr;}Test(const Test& t) {_a = (int*)malloc(10 * sizeof(int));for (int i = 0; i < 10; ++i) {_a[i] = t._a[i];}}// 赋值运算符重载Test& operator=(const Test& t) {for (int i = 0; i < 10; ++i) {_a[i] = t._a[i]; // 深拷贝}return *this;}
private:int* _a;int b;
};
值得注意的是,我们常常看到这样的代码
Test t2 = t1
。这里虽然使用了=
,但并不是赋值运算符重载,赋值运算符重载的定义是已实例化的对象被赋值,这上面代码是还没有实例化对象,所以是拷贝构造。
初始化列表
实例化对象时,编译器会通过构造函数来给成员变量一个初始值,这种行为只能称为赋值,并不能称为初始化,因为初始化只有一次,而构造函数中可以多次赋值。
- 语法:
- 以
:
开始,用,
分割数据成员列表,每个成员变量后面跟初始值(初始值)
。
- 以
初始化顺序
class Test {
public:Test(): _b(6), _a(_b) {}void print() {std::cout << _a << " " << _b << std::endl;}private:int _a;int _b;
};int main() {Test t;t.print();return 0;
}
如上述代码,按照初始化列表顺序初始化,则应是a = b = 6
这个结果。
而真实的结果是_a
是随机值,_b
是预期值6
。
可得出结论: 成员变量的初始化顺序和初始化列表顺序无关,和声明顺序有关。