C++面向对象编程
类与对象
定义一个类本质上是定义一个数据类型的蓝图
using namespace std;
/*
C++ 中的类&对象
*/
class Box { // define a class
public:
double length; // 箱子长度
double width; // 箱子宽度
double height; // 箱子高度
};
int main()
{
// 定义C++中的对象
Box Box1;
// box1详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.width = 7.0;
double volume = Box1.height * Box1.width * Box1.length;
cout << "Box1 的体积:" << volume << endl;
return 0;
}
编译上述程序可以得到
Box1 的体积:210
类的成员函数
类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
class Box { // define a class
public:
double length; // 箱子长度
double width; // 箱子宽度
double height; // 箱子高度
// 类的成员函数
double getVolume(void); // 返回体积
};
double Box::getVolume(void) {
return length * height * width;
}
类的访问修饰符
数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记public、private、protected
来指定的。关键字 public、private、protected
称为访问修饰符。
公有成员 public
是指不需要类的成员函数,在类外部可以直接访问和设置的成员。
私有成员 private
私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员,不能被派生类访问 。
class Box { // define a class
public:
double length; // 箱子长度
double width; // 箱子宽度
double height; // 箱子高度
// 类的成员函数
double getVolume(void); // 返回体积
double setVolume(void); // 设置对象中的volume私有变量
private:
double volume; // 箱子体积
};
double Box::setVolume(void) {
volume = length * height * width;
}
double Box::getVolume(void) {
return length * height * width;
}
int main()
{
// 定义C++中的对象
Box Box1;
// box1详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.width = 7.0;
Box1.setVolume();
cout << Box1.volume << endl;
double volume = Box1.height * Box1.width * Box1.length;
cout << "Box1 的体积:" << volume << endl;
return 0;
}
我们会发现cout << Box1.volume << endl;
这一行会报错,因为不允许我们直接调用那个成员变量。
保护成员 protected
其和private
很类似,但是不同在于,在继承的类中,protected
中的变量和成员函数是可以被调用的。
// 学习CPP.cpp: 定义控制台应用程序的入口点。
//
#include "stdafx.h"
using namespace std;
class Box {
public:
int height;
int length;
void setProperties(void);
protected:
int width;
};
void Box::setProperties(void) {
height = 5;
width = 4;
length = 3;
}
class SmallBox :public Box {
public:
void printVolume(void);
};
void SmallBox::printVolume(void) {
cout << height * width*length << endl;
}
int main() {
SmallBox box;
box.setProperties();
box.printVolume();
return 0;
}
继承Box
后的SmallBox
仍可以访问width
。
类构造函数和析构函数
构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
// 学习CPP.cpp: 定义控制台应用程序的入口点。
//
#include "stdafx.h"
using namespace std;
class Box {
public:
int height;
int length;
int width;
Box(int height,int length,int width); // 构造函数
~Box(); // 析构函数
};
Box::Box(int height, int length, int width) : height(height), length(length), width(width){ // 使用初始化列表来初始化字段
cout << "构造函数" << endl;
cout << "volume is:" << height * width*length << endl;
}
Box::~Box() {
cout << "析构函数" << endl;
}
int main() {
Box box(1,2,3);
return 0;
}
输出如下:
构造函数
volume is:6
析构函数
请按任意键继续. . .
拷贝构造函数
拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。
调用拷贝构造函数的情形
在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
一个对象作为函数参数,以值传递的方式传入函数体;
一个对象作为函数返回值,以值传递的方式从函数返回;
一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
为何需要拷贝函数?
class CExample {
public:
CExample() { pBuffer = NULL; nSize = 0; };
~CExample() { delete[] pBuffer; };
void init(int n) { pBuffer = new char[n]; nSize = n; };
private:
char *pBuffer;
int nSize;
};
int main() {
CExample theObj1;
theObj1.init(40);
// 现在需要另一个对象,并将它初始化为theObj1
CExample theObj2 = theObj1;
return 0;
}
我们观察上面这部分的代码,发现虽然theObj2
是以theObj1
的值初始化了,但是复制的仅仅是指针,指针指向的内存却还是一样的,意思就是,我要是theObj1
指向的内存发生了改变,则theObj2
的指向也发生了变化,这显然不是我们想要的,于是,我们便需要通过拷贝构造函数来解决这个问题。
class CExample {
public:
CExample() { pBuffer = NULL; nSize = 0; };
CExample(const CExample &Obj); // 拷贝构造函数
~CExample() { delete[] pBuffer; };
void init(int n) { pBuffer = new char[n]; nSize = n; };
private:
char *pBuffer;
int nSize;
};
CExample::CExample(const CExample &Obj) {
nSize = Obj.nSize;
pBuffer = new char[nSize]; // 分配内存
memcpy(pBuffer, Obj.pBuffer, nSize*sizeof(char));
};
int main() {
CExample theObj1;
theObj1.init(40);
// 现在需要另一个对象,并将它初始化为theObj1
CExample theObj2 = theObj1;
return 0;
}
而这样子通过拷贝构造函数,我们便可以得到完全一样,但是各自独立的对象了。
友元函数
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:
class Box {
private:
int width;
int height;
int length;
public:
void setProperities();
friend void printVolume(Box box); // 友元函数
};
void Box::setProperities() {
width = 2;
height = 3;
length = 4;
}
void printVolume(Box box) {
cout << box.width * box.height * box.length << endl;
}
int main() {
Box box;
box.setProperities();
printVolume(box);
}
输出如下:24
。
优缺点 1、优点:能够提高效率,表达清晰、简单; 2、缺点:破坏了类的封装(尽量不要使用成员函数,除非不得已才使用友元函数)。
内联函数
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
inline int max(int a, int b) {
return a > b ? a : b;
}
this
指针
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
class Box {
private:
int width;
int height;
int length;
public:
int calVolume() { return width * height*length; };
void setProperities();
friend void printVolume(Box box); // 友元函数
bool compare() { return calVolume() > 20; };
};
void Box::setProperities() {
width = 2;
height = 3;
length = 4;
}
void printVolume(Box box) {
cout << box.width * box.height * box.length << endl;
}
int main() {
Box box;
box.setProperities();
cout << box.compare() << endl;
}
这段代码返回结果是1
;
class Box {
private:
int width;
int height;
int length;
public:
int calVolume() { return width * height*length; };
void setProperities();
friend void printVolume(Box box); // 友元函数
bool compare() { return this->calVolume() > 20; }; // 使用this
};
void Box::setProperities() {
width = 2;
height = 3;
length = 4;
}
void printVolume(Box box) {
cout << box.width * box.height * box.length << endl;
}
int main() {
Box box;
box.setProperities();
cout << box.compare() << endl;
}
这段代码同样返回了1
,实际上,我们在第一段代码直接调用calVolume()
的时候,就是隐式的调用了this
。
指向类的指针
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
静态成员
我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。
class Box {
public:
static int count;
Box();
};
Box::Box() {
cout << "create a object" << endl;
count++;
}
// 初始化静态变量
int Box::count = 0;
int main() {
Box box1;
Box box2;
cout << "has " << Box::count << " objects" << endl;
}
输出如下:
create a object
create a object
has 2 objects
继承
基类和派生类
class Shape {
public:
void setWidth(int w);
void setHeight(int h);
protected:
int width;
int height;
};
void Shape::setHeight(int h) {
height = h;
};
void Shape::setWidth(int w) {
width = w;
}
// 派生类
class Rectangle :public Shape {
public:
int getArea() {
return width * height;
};
};
int main(){
Rectangle rec;
rec.setHeight(4);
rec.setWidth(5);
// 输出对象面积
cout << "对象面积为:" << rec.getArea() << endl;
return 0;
}
访问控制和继承
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
继承类型
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
重载运算符和重载函数
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
函数重载
class printData {
// 默认使用private修饰
public:
void print(int target) {
cout << "整数为:" << target << endl;
};
void print(double tar) {
cout << "浮点数为:" << tar << endl;
};
};
int main() {
printData p;
p.print(2.3);
p.print(2);
}
编译运行结果如下:
浮点数为:2.3
整数为:2
运算符重载
您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator
和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
class Box {
public:
int getVolume() {
return height * weight*length;
};
Box() {
// init
height = 5;
weight = 4;
length = 3;
};
Box operator+(const Box &b) {
Box box;
box.length = length + b.length;
box.weight = weight + b.weight;
box.height = height + b.height;
return box;
}
private:
int height;
int weight;
int length;
};
int main() {
Box box1;
Box box2;
Box box3;
box3 = box1 + box2;
cout << "Box3 的体积为:" << box3.getVolume() << endl;
return 0;
}
输出为:
Box3 的体积为:480
多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
class Shape {
protected:
int width;
int height;
public:
Shape(int a = 0, int b = 0) : width(a), height(b) {};
int area() {
cout << "Parent Class area" << endl;
return 0;
}
};
class Rectangle : public Shape {
public:
Rectangle(int a=0, int b=0) :Shape(a, b) {};
int area() {
cout << "Rectangle Class area" << endl;
return 0;
}
};
int main() {
Shape *shape;
Rectangle rec(10, 7);
// 存储矩形的地址
shape = &rec;
shape->area();
return 0;
}
运行结果如下:
Parent Class area
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
class Shape {
protected:
int width;
int height;
public:
Shape(int a = 0, int b = 0) : width(a), height(b) {};
virtual int area() { // 增加了virtual
cout << "Parent Class area" << endl;
return 0;
}
};
class Rectangle : public Shape {
public:
Rectangle(int a=0, int b=0) :Shape(a, b) {};
int area() {
cout << "Rectangle Class area" << endl;
return 0;
}
};
int main() {
Shape *shape;
Rectangle rec(10, 7);
// 存储矩形的地址
shape = &rec;
shape->area();
return 0;
}
这段代码的输出为:
Rectangle Class area
而我们得到了想要的结果,我们做的仅仅是将int area()
前面加上了virtual
将静态链接改为了动态链接。
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚函数
虚函数 是在基类中使用关键字virtual
声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
纯虚函数
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
我们可以把基类中的虚函数area()
改写如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
= 0
告诉编译器,函数没有主体,上面的虚函数是纯虚函数。