C++中的类与对象
类和对象的基本概念
C 和 C++中 struct 区别
c 语言
struct
只有变量
c++语言
struct
既有变量,也有函数
类的封装
我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种事物组成,我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:事和物抽象到计算机程序中,也就是用程序语言表示现实的事物。
那么现在问题是如何用程序语言来表示现实事物?现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为,所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物。
比如我们要表示人这个对象,在
c
语言中,我们可以这么表示
:
typedef struct _Person{
char name[64];
int age;
}Person;
typedef struct _Aninal{
char name[64];
int age;
int type; //动物种类
}Ainmal;
void PersonEat(Person* person){
printf("%s 在吃人吃的饭!\n",person->name);
}
void AnimalEat(Ainmal* animal){
printf("%s 在吃动物吃的饭!\n", animal->name);
}
int main(){
Person person;
strcpy(person.name, "小明");
person.age = 30;
AnimalEat(&person);
return EXIT_SUCCESS;
}
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的 行为,这样我们就表示出来一个事物,在 c
语言中,行为和属性是分开的,也就是 说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat 可以调用
Person
数据
AnimalEat
也可以调用
Person
数据,那么万一 调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属 性和行为的对象。 假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道, 那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和 函数增加访问权限。
封装
1. 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
2. 对变量和函数进行访问控制
访问权限
3. 在类的内部
(
作用域范围内
)
,没有访问权限之分,所有成员可以相互访问
4. 在类的外部
(
作用域范围外
)
,访问权限才有意义:
public
,
private
,
protected
5. 在类的外部,只有
public
修饰的成员才能被访问,在没有涉及继承与派生时, private 和
protected
是同等级的,外部不允许访问
对象的构造和析构
初始化和清理
我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置, 也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。
从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态, 当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。
那么我们 c++
中
OO
思想也是来源于现实,是对现实事物的抽象模拟,具体来说, 当我们创建对象的时候,
这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时, 对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安 全问题。c++
为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你 不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会 更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。
构造函数和析构函数
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动 调用,无须手动调用。
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
构造函数函数名和类名相同,没有返回值,不能有 void
,但可以有参数。
ClassName(){}
析构函数语法:
析构函数函数名是在类名前面加”~”
组成
,
没有返回值,不能有
void,
不能有参数, 不能重载。
~ClassName(){}
class Person{
public:
Person(){
cout << "构造函数调用!" << endl;
pName = (char*)malloc(sizeof("John"));
strcpy(pName, "John");
mTall = 150;
mMoney = 100;
}
~Person(){
cout << "析构函数调用!" << endl;
if (pName != NULL){
free(pName);
pName = NULL;
}
}
public:
char* pName;
int mTall;
int mMoney;
};
void test(){
Person person;
cout << person.pName << person.mTall << person.mMoney << endl;
}
构造函数的分类及调用
按参数类型:分为无参构造函数和有参构造函数
按类型分类:普通构造函数和拷贝构造函数
(
复制构造函数
)
class Person{
public:
Person(){
cout << "no param constructor!" << endl;
mAge = 0;
}
//有参构造函数
Person(int age){
cout << "1 param constructor!" << endl;
mAge = age;
}
//拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象
Person(const Person& person){
cout << "copy constructor!" << endl;
mAge = person.mAge;
}
//打印年龄
void PrintPerson(){
cout << "Age:" << mAge << endl;
}
private:
int mAge;
};
//1. 无参构造调用方式
void test01(){
//调用无参构造函数
Person person1;
person1.PrintPerson();
//无参构造函数错误调用方式
//Person person2();
//person2.PrintPerson();
}
//2. 调用有参构造函数
void test02(){
//第一种 括号法,最常用
Person person01(100);
person01.PrintPerson();
//调用拷贝构造函数
Person person02(person01);
person02.PrintPerson();
//第二种 匿名对象(显示调用构造函数)
Person(200); //匿名对象,没有名字的对象
Person person03 = Person(300);
person03.PrintPerson();
//注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型
Person person06(Person(400)); //等价于 Person person06 =
Person(400);
person06.PrintPerson();
//第三种 =号法 隐式转换
Person person04 = 100; //Person person04 = Person(100)
person04.PrintPerson();
//调用拷贝构造
Person person05 = person04; //Person person05 = Person(person04)
person05.PrintPerson();
}
b
为
A
的实例化对象
,A a = A(b)
和
A(b)
的区别?
当
A(b)
有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接 的时候,编译器认为你 A(b)
等价于
A b.
注意:不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确:
class Teacher{
public:
Teacher(){
cout << "默认构造函数!" << endl;
}
Teacher(const Teacher& teacher){
cout << "拷贝构造函数!" << endl;
}
public:
int mAge;
};
void test(){
Teacher t1;
//error C2086:“Teacher t1”: 重定义
Teacher(t1); //此时等价于 Teacher t1;
}
拷贝构造函数的调用时机
对象以值传递的方式传给函数参数
函数局部对象以值传递的方式从函数返回(vs debug
模式下调用一次拷贝构造,
qt 不调用任何构造)
用一个对象初始化另一个对象
class Person{
public:
Person(){
cout << "no param contructor!" << endl;
mAge = 10;
}
Person(int age){
cout << "param constructor!" << endl;
mAge = age;
}
Person(const Person& person){
cout << "copy constructor!" << endl;
mAge = person.mAge;
}
~Person(){
cout << "destructor!" << endl;
}
public:
int mAge;
};
//1. 旧对象初始化新对象
void test01(){
Person p(10);
Person p1(p);
Person p2 = Person(p);
Person p3 = p; // 相当于 Person p2 = Person(p);
}
//2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造
void doBussiness(Person p){}
void test02(){
Person p(10);
doBussiness(p);
}
//3. 函数返回局部对象
Person MyBusiness(){
Person p(10);
cout << "局部 p:" << (int*)&p << endl;
return p;
}
void test03(){
//vs release、qt 下没有调用拷贝构造函数
//vs debug 下调用一次拷贝构造函数
Person p = MyBusiness();
cout << "局部 p:" << (int*)&p << endl;
}
[Test03
结果说明
:]
编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).
在
vs debug模式下并没有进行这种优化,所以函数 MyBusiness
中创建
p
对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成 一个临时 Person
对象返回,然后调用
p
的析构函数。
我们从常理来分析的话,这个匿名对象和这个局部的 p
对象是相同的两个对象, 那么如果能直接返回 p
对象,就会省去一个拷贝构造和一个析构函数的开销,在 程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。
所以在这里,编译器偷偷帮我们做了一层优化:
当我们这样去调用: Person p = MyBusiness();
编译器偷偷将我们的代码更改为
:
void MyBussiness(Person& _result){
_result.X:X(); //调用 Person 默认拷贝构造函数
//.....对_result 进行处理
return;
}
int main(){
Person p; //这里只分配空间,不初始化
MyBussiness(p);
}
构造函数调用规则
默认情况下,
c++
编译器至少为我们写的类增加
3
个函数
1.默认构造函数
(
无参,函数体为空
)
2.默认析构函数
(
无参,函数体为空
)
3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
如果用户定义拷贝构造函数,c++
不会再提供任何默认构造函数
如果用户定义了普通构造(
非拷贝
)
,
c++
不在提供默认无参构造,但是会提供默认拷贝构造
深拷贝和浅拷贝
浅拷贝
同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然 是独立的两个对象,这种情况被称为浅拷贝.
一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配 的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自 定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
class Person{
public:
Person(char* name,int age){
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName,name);
mAge = age;
}
//增加拷贝构造函数
Person(const Person& person){
pName = (char*)malloc(strlen(person.pName) + 1);
strcpy(pName, person.pName);
mAge = person.mAge;
}
~Person(){
if (pName != NULL){
free(pName);
}
}
private:
char* pName;
int mAge;
};
void test(){
Person p1("Edward",30);
//用对象 p1 初始化对象 p2,调用 c++提供的默认拷贝构造函数
Person p2 = p1;
}
多个对象构造和析构
初始化列表
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用
:
class Person{
public:
#if 0
//传统方式初始化
Person(int a,int b,int c){
mA = a;
mB = b;
mC = c;
}
#endif
//初始化列表方式初始化
Person(int a, int b, int c):mA(a),mB(b),mC(c){}
void PrintPerson(){
cout << "mA:" << mA << endl;
cout << "mB:" << mB << endl;
cout << "mC:" << mC << endl;
}
private:
int mA;
int mB;
int mC;
};
注意:初始化成员列表(参数列表)只能在构造函数使用。
类对象作为成员
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象, 叫做对象成员。
C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,
c++编译器必 须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?
那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:对于子类调用构造函数,c++
为此提供了专门的语法,即构造 函数初始化列表。
当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关) 依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是 说,先调用对象成员的构造函数,再调用本身的构造函数。
析构函数和构造函数调用顺序相反,先构造,后析构
//汽车类
class Car{
public:
Car(){
cout << "Car 默认构造函数!" << endl;
mName = "大众汽车";
}
Car(string name){
cout << "Car 带参数构造函数!" << endl;
mName = name;
}
~Car(){
cout << "Car 析构函数!" << endl;
}
public:
string mName;
};
//拖拉机
class Tractor{
public:
Tractor(){
cout << "Tractor 默认构造函数!" << endl;
mName = "爬土坡专用拖拉机";
}
Tractor(string name){
cout << "Tractor 带参数构造函数!" << endl;
mName = name;
}
~Tractor(){
cout << "Tractor 析构函数!" << endl;
}
public:
string mName;
};
//人类
class Person{
public:
#if 1
//类 mCar 不存在合适的构造函数
Person(string name){
mName = name;
}
#else
//初始化列表可以指定调用构造函数
Person(string carName, string tracName, string name) :
mTractor(tracName), mCar(carName), mName(name){
cout << "Person 构造函数!" << endl;
}
#endif
void GoWorkByCar(){
cout << mName << "开着" << mCar.mName << "去上班!" << endl;
}
void GoWorkByTractor(){
cout << mName << "开着" << mTractor.mName << "去上班!" << endl;
}
~Person(){
cout << "Person 析构函数!" << endl;
}
private:
string mName;
Car mCar;
Tractor mTractor;
};
void test(){
//Person person("宝马", "东风拖拉机", "赵四");
Person person("刘能");
person.GoWorkByCar();
person.GoWorkByTractor();
}
explicit 关键字
c++提供了关键字
explicit
,禁止通过构造函数进行的隐式转换。声明为
explicit
的构造函数不能在隐式转换中使用。
[explicit
注意
]
explicit 用于修饰构造函数
,
防止隐式转化。
是针对单参数的构造函数(
或者除了第一个参数外其余参数都有默认值的多参构造
) 而言。
class MyString{
public:
explicit MyString(int n){
cout << "MyString(int n)!" << endl;
}
MyString(const char* str){
cout << "MyString(const char* str)" << endl;
}
};
int main(){
//给字符串赋值?还是初始化?
//MyString str1 = 1;
MyString str2(10);
//寓意非常明确,给字符串赋值
MyString str3 = "abcd";
MyString str4("abcd");
return EXIT_SUCCESS;
}
动态对象创建
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然 c
早就提供了动态内存分配(
dynamic memory allocation
)
,
函数
malloc
和
free 可以在运行时从堆中分配存储单元。
然而这些函数在 c++
中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
对象创建
当创建一个
c++
对象时会发生两件事
:
1. 为对象分配内存
2. 调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。c++
强迫我们这么 做是因为使用未初始化的对象是程序出错的一个重要原因。
C 动态分配内存方法
为了在运行时动态分配内存,c
在他的标准库中提供了一些函数
,malloc
以及 它的变种 calloc
和
realloc,
释放内存的
free,
这些函数是有效的、但是原始的, 需要程序员理解和小心使用。为了使用 c
的动态内存分配函数在堆上创建一个 类的实例,我们必须这样做
class Person{
public:
Person(){
mAge = 20;
pName = (char*)malloc(strlen("john")+1);
strcpy(pName, "john");
}
void Init(){
mAge = 20;
pName = (char*)malloc(strlen("john")+1);
strcpy(pName, "john");
}
void Clean(){
if (pName != NULL){
free(pName);
}
}
public:
int mAge;
char* pName;
};
int main(){
//分配内存
Person* person = (Person*)malloc(sizeof(Person));
if(person == NULL){
return 0;
}
//调用初始化函数
person->Init();
//清理对象
person->Clean();
//释放 person 对象
free(person);
return EXIT_SUCCESS;
}
问题:
1) 程序员必须确定对象的长度。
2) malloc 返回一个
void
指针,
c++
不允许将
void
赋值给其他任何指针,必须强转。
3) malloc 可能申请内存失败,所以必须判断返回值来确保内存分配成功。
4) 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化
(
构造 函数是由编译器调用)
,用户有可能忘记调用初始化函数。
c 的动态内存分配函数太复杂,容易令人混淆,是不可接受的,
c++
中我们推荐使 用运算符 new
和
delete
new operator
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为 new 的运算符里。当用
new
创建一个对象时,它就在堆里为对象分配内存并调用 构造函数完成初始化。
Person* person =
new
Person;
相当于
:
Person* person = (Person*)malloc(
sizeof
(Person));
if
(person == NULL){
return
0
;
}
person->Init(); 构造函数
New
操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式 确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带 有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对 象一样简单。
delete operator
new 表达式的反面是
delete
表达式。
delete
表达式先调用析构函数,然后释放内 存。正如 new
表达式返回一个指向对象的指针一样,
delete
需要一个对象的地址。
delete 只适用于由
new
创建的对象。
如果使用一个由 malloc
或者
calloc
或者
realloc
创建的对象使用
delete,
这个行为是未定义的。因为大多数 new
和
delete
的实现机制都使用了
malloc
和
free,
所以很 可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是 NULL,
将不发生任何事,因此建议在删除指针后,立 即把指针赋值为 NULL
,以免对它删除两次,对一些对象删除两次可能会产生某些 问题。
class Person{
public:
Person(){
cout << "无参构造函数!" << endl;
pName = (char*)malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char* name, int age){
cout << "有参构造函数!" << endl;
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName, name);
mAge = age;
}
void ShowPerson(){
cout << "Name:" << pName << " Age:" << mAge << endl;
}
~Person(){
cout << "析构函数!" << endl;
if (pName != NULL){
delete pName;
pName = NULL;
}
}
public:
char* pName;
int mAge;
};
void test(){
Person* person1 = new Person;
Person* person2 = new Person("John",33);
person1->ShowPerson();
person2->ShowPerson();
delete person1;
delete person2;
}
用于数组的 new 和 delete
使用 new 和 delete 在堆上创建数组非常容易。
//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100];
//创建整型数组并初始化
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
class Person{
public:
Person(){
pName = (char*)malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char* name, int age){
pName = (char*)malloc(sizeof(name));
strcpy(pName, name);
mAge = age;
}
~Person(){
if (pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test(){
//栈聚合初始化
Person person[] = { Person("john", 20), Person("Smith", 22) };
cout << person[1].pName << endl;
//创建堆上对象数组必须提供构造函数
Person* workers = new Person[20];
}
delete void*可能会出错
如果对一个 void*
指针执行
delete
操作,这将可能成为一个程序错误,除非指针指 向的内容是非常简单的,因为它将不执行析构函数.
以下代码未调用析构函数,导致可用内存减少。
class Person{
public:
Person(char* name, int age){
pName = (char*)malloc(sizeof(name));
strcpy(pName,name);
mAge = age;
}
~Person(){
if (pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test(){
void* person = new Person("john",20);
delete person;
}
问题:malloc
、
free
和
new
、
delete
可以混搭使用吗?也就是说
malloc
分配的内 存,可以调用 delete
吗?通过
new
创建的对象,可以调用
free
来释放吗?
使用 new 和 delete 采用相同形式
Person* person =
new
Person[
10
];
delete
person;
以上代码有什么问题吗?(vs
下直接中断、
qt
下析构函数调用一次
)
使用了 new
也搭配使用了
delete
,问题在于
Person
有
10
个对象,那么其他
9
个 对象可能没有调用析构函数,也就是说其他 9
个对象可能删除不完全,因为它们 的析构函数没有被调用。
我们现在清楚使用 new
的时候发生了两件事
:
一、分配内存;二、调用构造函数, 那么调用 delete
的时候也有两件事:一、析构函数;二、释放内存。
那么刚才我们那段代码最大的问题在于:person
指针指向的内存中到底有多少个 对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person
指针 指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局 是不同的。更明确的说,数组所用的内存通常还包括“
数组大小记录
”
,使得
delete 的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数 组对象的内存布局可理解为下图:
本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。
当我们使用一个 delete
的时候,我们必须让
delete
知道指针指向的内存空间中是 否存在一个“
数组大小记录
”
的办法就是我们告诉它。当我们使用
delete[]
,那么 delete 就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论
:
如果在 new
表达式中使用
[]
,必须在相应的
delete
表达式中也使用
[].
如果在
new 表达式中不使用[],
一定不要在相应的
delete
表达式中使用
[]