4.1 类和对象的基本概念
4.1.1 C和C++中struct区别
- c语言struct中只有变量
- c++语言struct中既有变量,也有函数
4.1.2 类的封装
我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种事物组成,我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:事和物抽象到计算机程序中,也就是用程序语言表示现实的事物。
那么现在问题是如何用程序语言来表示现实事物?现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为,所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物。
比如我们要表示人这个对象,在c语言中,我们可以这么表示:
#include <iostream>
using namespace std;
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 0;
}
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在c语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象。
假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。
所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限。
1.封装
(1) 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
(2)对变量和函数进行访问控制
2.访问权限
(1)在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
(2)在类的外部(作用域范围外),访问权限才有意义:public,private,protected
(3)在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时, private和protected是同等级的,外部不允许访问
//封装两层含义
//1. 属性和行为合成一个整体
//2. 访问控制,现实事物本身有些属性和行为是不对外开放
class Person
{
//人具有的行为(函数)
public:
void Dese()
{
cout << "我有钱,年轻,个子又高,就爱嘚瑟!" << endl;
}
//人的属性(变量)
public:
int mTall; //多高,可以让外人知道
protected:
int mMoney; // 有多少钱,只能儿子孙子知道
private:
int mAge; //年龄,不想让外人知道
};
int main()
{
Person p;
p.mTall = 220;
//p.mMoney 保护成员外部无法访问
//p.mAge 私有成员外部无法访问
p.Dese();
return 0;
}
输出结果
我有钱,年轻,个子又高,就爱嘚瑟!
[struct和class的区别?] class默认访问权限为private,struct默认访问权限为public。 |
class A
{
int mAge;
};
struct B
{
int mAge;
};
void test()
{
A a;
B b;
//a.mAge; //无法访问私有成员
b.mAge; //可正常外部访问
}
4.1.3 将成员变量设置为private
1.可赋予客户端访问数据的一致性。
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
2.可细微划分访问控制。
使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。
4.2 面向对象程序设计案例
4.2.1 设计立方体类
设计立方体类(Cube),求出立方体的面积( 2*a*b + 2*a*c + 2*b*c )和体积( a * b * c),分别用全局函数和成员函数判断两个立方体是否相等。
//立方体类
class Cub
{
public:
void setL(int l)
{ mL = l; }
void setW(int w)
{ mW = w; }
void setH(int h)
{ mH = h; }
int getL() { return mL; }
int getW() { return mW; }
int getH() { return mH; }
//立方体面积
int caculateS() { return (mL * mW + mL * mH + mW * mH) * 2; }
//立方体体积
int caculateV() { return mL * mW * mH; }
//成员方法
bool CubCompare(Cub& c)
{
if (getL() == c.getL() && getW() == c.getW() && getH() == c.getH())
{
return true;
}
return false;
}
private:
int mL; //长
int mW; //宽
int mH; //高
};
//比较两个立方体是否相等
bool CubCompare(Cub& c1, Cub& c2)
{
if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH())
{
return true;
}
return false;
}
void test()
{
Cub c1, c2;
c1.setL(10);
c1.setW(20);
c1.setH(30);
c2.setL(20);
c2.setW(20);
c2.setH(30);
cout << "c1面积:" << c1.caculateS() << " 体积:" << c1.caculateV() << endl;
cout << "c2面积:" << c2.caculateS() << " 体积:" << c2.caculateV() << endl;
//比较两个立方体是否相等
if (CubCompare(c1, c2))
{
cout << "c1和c2相等!" << endl;
}
else
{
cout << "c1和c2不相等!" << endl;
}
if (c1.CubCompare(c2))
{
cout << "c1和c2相等!" << endl;
}
else
{
cout << "c1和c2不相等!" << endl;
}
}
int main()
{
test();
return 0;
}
输出结果
c1面积:2200 体积:6000
c2面积:3200 体积:12000
c1和c2不相等!
c1和c2不相等!
4.2.2 点和圆的关系
设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。
假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:
- 点在圆上:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) == r*r
- 点在圆内:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) < r*r
- 点在圆外:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) > r*r
//点类
class Point
{
public:
void setX(int x)
{ mX = x; }
void setY(int y)
{ mY = y; }
int getX()
{ return mX; }
int getY()
{ return mY; }
private:
int mX;
int mY;
};
//圆类
class Circle
{
public:
void setP(int x, int y)
{
mP.setX(x);
mP.setY(y);
}
void setR(int r)
{ mR = r; }
Point& getP()
{ return mP; }
int getR()
{ return mR; }
//判断点和圆的关系
void IsPointInCircle(Point& point)
{
int distance = (point.getX() - mP.getX()) * (point.getX() - mP.getX()) + (point.getY() - mP.getY()) * (point.getY() - mP.getY());
int radius = mR * mR;
if (distance < radius)
{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆内!" << endl;
}
else if (distance > radius)
{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆外!" << endl;
}
else
{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆上!" << endl;
}
}
private:
Point mP; //圆心
int mR; //半径
};
void test()
{
//实例化圆对象
Circle circle;
circle.setP(20, 20);
circle.setR(5);
//实例化点对象
Point point;
point.setX(25);
point.setY(20);
circle.IsPointInCircle(point);
}
int main()
{
test();
return 0;
}
输出结果
Point(25,20)在圆上!
4.3 对象的构造和析构
4.3.1 初始化和清理
我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置,也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。
从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。
那么我们c++中OO思想也是来源于现实,是对现实事物的抽象模拟,具体来说,当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。
4.3.2 构造函数和析构函数
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
|
析构函数语法:
|
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;
}
int main()
{
test();
return 0;
}
输出结果
构造函数调用!
John 150 100
析构函数调用!
4.3.3 构造函数的分类及调用
- 按参数类型:分为无参构造函数和有参构造函数
- 按类型分类:普通构造函数和拷贝构造函数(复制构造函数)
class Person
{
public:
Person()
{
cout << "no param constructor!" << endl;
mAge = 0;
}
//有参构造函数
Person(int age)
{
cout << "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 person04(Person(400)); //等价于 Person person06 = Person(400);
person04.PrintPerson();
//第三种 =号法 隐式转换
Person person05 = 100; //Person person04 = Person(100)
person04.PrintPerson();
//调用拷贝构造
Person person06 = person05; //Person person05 = Person(person04)
person06.PrintPerson();
}
int main()
{
test01();
test02();
return 0;
}
输出结果
no param constructor!
Age:0
param constructor!
Age:100
copy constructor!
Age:100
param constructor!
param constructor!
Age:300
param constructor!
Age:400
param constructor!
Age:400
copy constructor!
Age:100
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;
Teacher(t1); //error C2086:“Teacher t1”: 重定义,此时等价于 Teacher t1;
}
4.3.4 拷贝构造函数的调用时机
- 对象以值传递的方式传给函数参数
- 函数局部对象以值传递的方式从函数返回(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);
return p;
}
void test03()
{
//vs release、qt下没有调用拷贝构造函数
//vs debug下调用一次拷贝构造函数
Person p = MyBusiness();
}
int main()
{
test01();
test02();
test03();
return 0;
}
输出结果
param constructor!
copy constructor!
copy constructor!
copy constructor!
destructor!
destructor!
destructor!
destructor!
param constructor!
copy constructor!
destructor!
destructor!
param constructor!
copy constructor!
destructor!
destructor!
[Test03结果说明:] 编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数。 我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。 所以在这里,编译器偷偷帮我们做了一层优化: 当我们这样去调用: Person p = MyBusiness(); 编译器偷偷将我们的代码更改为: |
void MyBussiness(Person& _result)
{
_result.X:X(); //调用Person默认拷贝构造函数
return;
}
int main()
{
Person p; //这里只分配空间,不初始化
MyBussiness(p);
}
4.3.5 构造函数调用规则
(1)默认情况下,c++编译器至少为我们写的类增加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对类中非静态成员属性简单值拷贝
(2)如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
(3)如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造
4.3.6 深拷贝和浅拷贝
4.3.6.1 浅拷贝
同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝。
一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
4.3.6.2 深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间。
class Person
{
public:
Person(const 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);
}
}
void Display()
{
cout << pName << " " << mAge << endl;
}
private:
char* pName;
int mAge;
};
void test()
{
Person p1("Jack", 30);
//用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数
Person p2 = p1;
p1.Display();
p2.Display();
}
输出结果
Jack 30
Jack 30
4.3.7 多个对象构造和析构
4.3.7.1 初始化列表
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
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;
};
注意:初始化成员列表(参数列表)只能在构造函数使用。 |
4.3.7.2 类对象作为成员
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
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(const 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.GoWorkByCar();
person.GoWorkByTractor();
}
int main()
{
test();
return 0;
}
输出结果
Car 默认构造函数!
Tractor 默认构造函数!
刘能开着大众汽车去上班!
刘能开着爬土坡专用拖拉机去上班!
Person 析构函数!
Tractor 析构函数!
Car 析构函数!
4.3.8 explicit关键字
c++提供了关键字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 0;
}
输出结果
MyString(int n)!
MyString(const char* str)
MyString(const char* str)
4.3.9 动态对象创建
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
4.3.9.1 对象创建
当创建一个c++对象时会发生两件事:
- 为对象分配内存
- 调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。
4.3.9.2 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 0;
}
问题:
|
c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和 delete.
4.3.9.3 new operator
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。