C++面向对象之"类"
上一节讲了头文件。头文件
本节正式进入面向对象的编程,类。
- 面向过程: 根据程序执行的先后顺序,来设计所有细节
缺点:开发大型项目时,会导致难把控所有的细节。 - 面向对象: 一种全新的开发方式。
类的基础
-
最重要的一个概念:“类” class
类是一种特殊的“数据结构",不是一个具体的数据。
和基本的数据类型不同 (char/int/float)
在实现类的时候一般把类与类的方法声明放到.h文件中,它的实现放在一个.cpp文件中。 -
使用class 类名{
public:
成员或方法
private:
成员或方法
};
-
类的设计:
-
此处定义一个 "dog类"
#include <iostream> #include <string> using namespace std; //定义类 class Dog{ public://公有的 void eat(); //吃 void sleep(); //睡 void play(); //玩 int getWeight(); //获取体重 string getName(); private://私有的 无法被内部直接访问 int Weight;//体重 string name; //名字 }; //实现方法 //成员函数前面要加Dog::表明该方法是类Dog的方法 void Dog::eat(){ cout << "吃肉" << endl; } void Dog::sleep(){ cout << "睡觉" << endl; } void Dog::play(){ cout << "玩" << endl; } //通过两个内部public方法访问private成员 int Dog::getWeight(){ return Weight; } string Dog::getName(){ return name; } int main(){ //定义一个类的对象 Dog dog; //失败,无法访问private成员 //dog.name; //通过public方法访问私有成员 dog.getName(); return 0; }
此处定义一个Dog类,有类的public成员函数,有private成员。
-
使用类中的成员需要
类名.方法或者成员
调用 -
注意: private成员对外部无法访问,例如上面main函数中的dog.name失败!
特点:安全,无法在外部被修改,只能通过内部方法修改。 -
将private改成public可以让数据变得可以访问,但是不建议这么做,会降低安全性
类的默认构造函数
-
在创建对象时初始化里面的数据成员就需要构造函数,不然就没有初始化。
-
构造函数可以定义很多种构造函数
-
在创建对象时,自动调用构造函数。
#include <iostream> #include <string> using namespace std; //定义类 class Dog{ public://公有的 //默认构造函数不定义会自动调用默认生成的 Dog(); //自定义的构造函数 Dog(const char* name, int weight); void eat(); //吃 void sleep(); //睡 void play(); //玩 int getWeight(); //获取体重 string getName(); private://私有的 无法被内部直接访问 int Weight;//体重 string name; //名字 }; //实现方法 //默认构造函数 Dog::Dog(){ } Dog::Dog(const char* _name, int _weight){ this->name = _name; this->Weight = _weight; } //成员函数前面要加Dog::表明该方法是类Dog的方法 void Dog::eat(){ cout << "吃肉" << endl; } void Dog::sleep(){ cout << "睡觉" << endl; } void Dog::play(){ cout << "玩" << endl; } //通过两个内部public方法访问private成员 int Dog::getWeight(){ return Weight; } string Dog::getName(){ return name; } int main(){ //定义一个类的对象 Dog dog("旺财", 20); //失败,无法访问private成员 //dog.name; //通过public方法访问私有成员 dog.getName(); return 0; }
this
表示一个指针,代表了调用该方法的对象,比如Dog dog("旺财", 20);
中对象dog调用构造函数Dog(const char* _name, int _weight)
此时this->name
即为对象本身的name成员。 -
第一个构造函数
Dog::Dog()
;为默认的构造函数,可以缺省,第二个Dog::Dog(const char* _name, int _weight)
为人工合成的默认构造函数。
类的拷贝构造函数
-
拷贝构造函数就是相当于一种复制。自己不定义时,会自动生成默认的拷贝构造函数,具体的使用如下:
#include <iostream> #include <string> using namespace std; //定义类 class Dog{ public://公有的 //默认构造函数不定义会自动调用默认生成的 Dog(); //自定义的构造函数 Dog(const char* name, int weight); void eat(); //吃 void sleep(); //睡 void play(); //玩 int getWeight(); //获取体重 string getName(); private://私有的 无法被内部直接访问 int Weight;//体重 string name; //名字 }; //实现方法 //默认构造函数 Dog::Dog(){ } Dog::Dog(const char* _name, int _weight){ this->name = _name; this->Weight = _weight; } //成员函数前面要加Dog::表明该方法是类Dog的方法 void Dog::eat(){ cout << "吃肉" << endl; } void Dog::sleep(){ cout << "睡觉" << endl; } void Dog::play(){ cout << "玩" << endl; } //通过两个内部public方法访问private成员 int Dog::getWeight(){ return Weight; } string Dog::getName(){ return name; } int main(){ //定义一个类的对象并调用自定义的默认构造函数 Dog dog("旺财", 20); //失败,无法访问private成员 //dog.name; //通过public方法访问私有成员 cout << "dog的名字是:" << dog.getName() << endl; //下面两种都会调用拷贝构造函数 Dog dog1 = dog; Dog dog2(dog); cout << "dog1的名字是:" << dog.getName() << endl; cout << "dog2的名字是:" << dog.getName() << endl; return 0; }
输出结果:
dog的名字是:旺财 dog1的名字是:旺财 dog2的名字是:旺财
这里虽然没有定义拷贝构造函数,但是系统自动生成了拷贝构造函数并且调用。
缺点: 默认拷贝函数只是一种"浅拷贝"而非"深拷贝"。
"深浅"拷贝
为了让大家理解浅拷贝和深拷贝的区别,这里举个栗子:(暂时不主动释放new申请的空间)
#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test{ public: //默认构造函数 Test(const char* _str = "测试"){ //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //定义一个改变str成员的方法 void changeStr(const char* _str){ strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr(){ return str; } private: //字符串 char* str; }; int main(){ //定义两个对象 Test t1("测试1"); //将t1拷贝到t2 Test t2(t1); cout << "改变t2字符串前t1字符串为:" << t1.getStr() << endl; //改变t2的字符串,并获取t1的字符串 t2.changeStr("测试2"); cout << "改变t2字符串后t1字符串为:" << t1.getStr() << endl; return 0; }
输出结果:
改变t2字符串后t1字符串为:测试1 改变t2字符串后t1字符串为:测试2
此处由于"浅"拷贝的原因,改变t2导致t1跟着一起改变。具体解释就浅拷贝只是值的复制,所以拷贝后t2的str成员指向的内存空间和t1的str成员指向的内存空间是同一块内存空间,改变其中一个的值,另外一个会跟着变。
所以使用系统默认的拷贝构造函数有致命的缺陷! -
使用 自定义的拷贝构造函数("深"拷贝) 来规避这种缺陷。
#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test{ public: //默认构造函数 Test(const char* _str = "测试"){ //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //定义拷贝构造函数 Test(const Test& other){ if(other.str){ this->str = new char[strlen(other.str) + 1]; strcpy_s(this->str, strlen(other.str) + 1, other.str); } } //定义一个改变str成员的方法 void changeStr(const char* _str){ strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr(){ return str; } private: //字符串 char* str; }; int main(){ //定义两个对象 Test t1("测试1"); //将t1拷贝到t2 Test t2(t1); cout << "改变t2字符串前t1字符串为:" << t1.getStr() << endl; cout << "改变t2字符串前t2字符串为:" << t2.getStr() << endl; //改变t2的字符串,并获取t1的字符串 t2.changeStr("测试2"); cout << "改变t2字符串后t1字符串为:" << t1.getStr() << endl; cout << "改变t2字符串后t2字符串为:" << t2.getStr() << endl; return 0; }
输出结果:
改变t2字符串前t1字符串为:测试1 改变t2字符串前t2字符串为:测试1 改变t2字符串后t1字符串为:测试1 改变t2字符串后t2字符串为:测试2
这里成功采用深拷贝规避了浅拷贝带来的问题。
-
注意赋值构造函数(特殊的拷贝构造函数)也是浅拷贝。
#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test{ public: //默认构造函数 Test(const char* _str = "测试"){ //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //定义一个改变str成员的方法 void changeStr(const char* _str){ strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr(){ return str; } private: //字符串 char* str; }; int main(){ //定义两个对象 Test t1("测试1"); //将t1赋值给t2 Test t2 = t1; cout << "改变t2字符串前t1字符串为:" << t1.getStr() << endl; //改变t2的字符串,并获取t1的字符串 t2.changeStr("测试2"); cout << "改变t2字符串后t1字符串为:" << t1.getStr() << endl; return 0; }
输出结果:
改变t2字符串后t1字符串为:测试1 改变t2字符串后t1字符串为:测试2
这里也同要是浅拷贝,一个好的类,应该也同时有赋值构造函数来避免浅拷贝(实质上为一个=运算符的重载)。
#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test{ public: //默认构造函数 Test(const char* _str = "测试"){ //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //定义拷贝构造函数 Test(const Test& other){ this->str = new char[strlen(other.str) + 1]; strcpy_s(this->str, strlen(other.str) + 1, other.str); } //定义赋值构造函数 Test& operator=(const Test& other){ if(this == &other) return *this; if(other.str){ this->str = new char[strlen(other.str) + 1]; strcpy_s(this->str, strlen(other.str) + 1, other.str); } return *this; } //定义一个改变str成员的方法 void changeStr(const char* _str){ strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr(){ return str; } private: //字符串 char* str; }; int main(){ //定义两个对象 Test t1("测试1"); //将t1拷贝到t2 Test t2 = t1; cout << "改变t2字符串前t1字符串为:" << t1.getStr() << endl; cout << "改变t2字符串前t2字符串为:" << t2.getStr() << endl; //改变t2的字符串,并获取t1的字符串 t2.changeStr("测试2"); cout << "改变t2字符串后t1字符串为:" << t1.getStr() << endl; cout << "改变t2字符串后t2字符串为:" << t2.getStr() << endl; return 0; }
输出结果:
改变t2字符串前t1字符串为:测试1 改变t2字符串前t2字符串为:测试1 改变t2字符串后t1字符串为:测试1 改变t2字符串后t2字符串为:测试2
这里重写了等号运算符,让其能够接受类型为Test的参数,等效于
t2 =(t1)
;这里就把浅拷贝变成了深拷贝。避免了一些特殊情况。
类的析构函数
-
析构函数和构造函数相对,用来删除类的函数。
默认情况下也会自动生成。定义, 使用~类名()来定义。
缺陷,它不会自动释放类的对象申请的内存空间。#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test{ public: //默认构造函数 Test(const char* _str = "测试"){ //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //定义拷贝构造函数 Test(const Test& other){ this->str = new char[strlen(other.str) + 1]; strcpy_s(this->str, strlen(other.str) + 1, other.str); } //定义赋值构造函数 Test& operator=(const Test& other){ if(this == &other) return *this; if(other.str){ this->str = new char[strlen(other.str) + 1]; strcpy_s(this->str, strlen(other.str) + 1, other.str); } return *this; } //默认析构函数 ~Test(){ cout << str <<"调用析构函数" << endl; } //定义一个改变str成员的方法 void changeStr(const char* _str){ strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr(){ return str; } private: //字符串 char* str; }; int main(){ //定义一个指针指向t2的str的内存空间 char* point = NULL; //定义两个对象 Test t1("测试1"); //t2在大括号结束,自动调用析构函数 { Test t2("测试2"); point = t2.getStr(); } //访问t2.str的内存空间,此时 cout << "point的值:" << point << endl; return 0; }
输出结果:
测试2调用析构函数 point的值:测试2 测试1调用析构函数
这里即使是t2被析构了,仍然能够访问t2.str的内存空间,**可见析构函数不会把对象中分配好的空间给释放掉。**这会导致非常严重的后果——内存泄漏!!!
-
改进:改进析构函数,使其能够主动在对象的生命周期结束时释放掉已经分配的空间。
这里为了测试,拷贝构造函数采用浅拷贝方式。#include <iostream> #include <string.h> using namespace std; //为了方便,方法在类中实现 class Test { public: //默认构造函数 Test(const char* _str) { //包含一个结束符 str = new char[strlen(_str) + 1]; //拷贝_str字符串到str中 strcpy_s(str, strlen(_str) + 1, _str); } //默认析构函数 ~Test() { if (str) delete[] str; cout << str << "调用析构函数" << endl; str = NULL; } //定义一个改变str成员的方法 void changeStr(const char* _str) { strcpy_s(str, strlen(_str) + 1, _str); } //定义一个获取str的方法 char* getStr() { return str; } private: //字符串 char* str; }; int main() { //定义两个对象 Test t1; //t2在大括号结束,自动调用析构函数 { Test t2("测试2"); t1 = t2; } //访问t2.str的内存空间,此时 cout << "t1.str的值:" << t1.getStr() << endl; return 0; }
输出结果:一些奇怪的中午字符,乱码。所以这里成功使用析构函数释放掉了对象申请的空间。
本节介绍了类的基础,下节我们我们介绍类的函数成员。