C++ 面向对象
1 类 & 对象
C++ 在 C 语言的基础上增加了面向对象编程,类是 C++ 的核心特性,类用于指定对象的形式,是一种用户自定义的数据类型,是一种封装了数据和函数的组合。
1.1 类定义
class Box {
public:
double length;
double breadth;
double height;
};
关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。
声明类的对象,就像声明基本类型的变量一样。
Box box1;
Box box2;
类的对象的公共数据成员可以使用直接成员访问运算符 .
来访问。
#include <iostream>
class Box {
public:
double length;
double breadth;
double height;
double getV(void);
void setShape(double len, double bre, double hei);
};
double Box::getV(void) {
return length * breadth * height;
}
void Box::setShape(double len, double bre, double hei) {
length = len;
breadth = bre;
height = hei;
}
int main() {
Box box1;
Box box2;
double v = 0.0;
box1.height = 10.0;
box1.length = 12.0;
box1.breadth = 13.0;
v = box1.height * box1.length * box1.breadth;
std::cout << "V of box1: " << v << std::endl;
box2.setShape(4.0, 5.0, 6.0);
v = box2.getV();
std::cout << "V of box2: " << v << std::endl;
return 0;
}
输出:
V of box1: 1560
V of box2: 120
私有的成员和受保护的成员不能使用直接成员访问运算符 (.
) 来直接访问。
1.2 类成员函数
类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
成员函数可以定义在类定义内部,或者单独使用范围解析运算符 ::
来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline
标识符:
class Box {
public:
double length;
double breadth;
double height;
void setShape(double len, double bre, double hei);
double getV(void) {
return length * height * breadth;
}
};
也可以在类的外部使用范围解析运算符 ::
定义该函数。在 ::
运算符之前必须使用类名。用类名。调用成员函数是在对象上使用点运算符(.
),这样它就能操作与该对象相关的数据,如下所示:
Box myBox;
myBox.getV();
1.3 类访问修饰符
类成员可以被定义为 public、private 或 protected。一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。
class Base {
public:
// 公有成员
protected:
// 受保护成员
private:
// 私有成员
};
1.3.1 公有(public)成员
公有成员在程序中类的外部是可访问的,可以不使用任何成员函数来设置和获取公有变量的值:
class Line {
public:
double length;
void setLength(double len);
double getLength(void);
};
// 成员函数定义
double Line::getLength(void) {
return length ;
}
void Line::setLength( double len ) {
length = len;
}
Line line;
line.setLength(6.9);
// 不使用成员函数设置长度
line.length = 10.0;
1.3.2 私有(private)成员
私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。
#include <iostream>
class Box {
double width; // width 是一个私有成员
public:
double length;
void setWidth(double wid);
double getWidth( void );
};
// 成员函数定义
double Box::getWidth(void) {
return width ;
}
void Box::setWidth( double wid ) {
width = wid;
}
int main() {
Box box;
// 不使用成员函数设置长度
box.length = 10.0; // OK: 因为 length 是公有的
std::cout << "lenght: " << box.length << std::endl;
// 不使用成员函数设置宽度
// box.width = 10.0; // Error: 因为 width 是私有的
box.setWidth(10.0); // 使用成员函数设置宽度
std::cout << "width: " << box.getWidth() << std::endl; // 这里访问也只能通过成员函数进行
return 0;
}
一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。
1.3.3 protected(受保护)成员
protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。
#include <iostream>
class Box {
protected:
double width;
};
class SmallBox: Box {
public:
void setSmallWidth(double wid);
double getSmallWidth(void);
};
double SmallBox::getSmallWidth(void) {
return width;
}
void SmallBox::setSmallWidth(double wid) {
width = wid;
}
int main() {
SmallBox box;
// 使用成员函数设置宽度
box.setSmallWidth(4.0);
std::cout << "Width of box : " << box.getSmallWidth() << std::endl;
// 使用成员函数设置宽度
// box.width = 5.0; // Error
// std::cout << "Width of box : " << box.width << std::endl; // Error
return 0;
}
这里由于没有指明继承的方式,默认是按照 private 的方式来继承,因此继承到的 width 还是 protected。
1.3.4 继承中的特点
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
-
public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private(public 继承不改变继承自父类的成员权限)
-
protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private(protected 继承,继承自父类的成员,权限不高于 protected)
-
private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private(private 继承,继承自父类的成员,权限全部变成 private)
但无论哪种继承方式,下面两点都没有改变:
-
private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
-
protected 成员可以被派生类访问。
public 继承:
#include <iostream>
#include <assert.h>
using namespace std;
class A {
public:
int a;
A() {
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void func() {
cout << a << endl; //正确
cout << a1 << endl; //正确
cout << a2 << endl; //正确
cout << a3 << endl; //正确
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B: public A {
public:
int a;
B(int i) {
A();
a = i;
}
void fun(){
cout << a << endl; //正确,public成员
cout << a1 << endl; //正确,基类的public成员,在派生类中仍是public成员。
cout << a2 << endl; //正确,基类的protected成员,在派生类中仍是protected可以被派生类访问。
// cout << a3 << endl; //错误,基类的private成员不能被派生类访问。
}
};
int main() {
B b(10);
cout << b.a << endl; // OK
cout << b.a1 << endl; // OK
// cout << b.a2 << endl; // 错误,类外不能访问protected成员
// cout << b.a3 << endl; // 错误,类外不能访问private成员
return 0;
}
protected 继承:
#include <iostream>
#include <assert.h>
using namespace std;
class A {
public:
int a;
A() {
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void func() {
cout << a << endl; //正确
cout << a1 << endl; //正确
cout << a2 << endl; //正确
cout << a3 << endl; //正确
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B: protected A {
public:
int a;
B(int i) {
A();
a = i;
}
void fun(){
cout << a << endl; //正确,public成员
cout << a1 << endl; //正确,基类的public成员,在派生类中变成了protected,可以被派生类访
cout << a2 << endl; //正确,基类的protected成员,在派生类中仍是protected可以被派生类访问。
// cout << a3 << endl; //错误,基类的private成员不能被派生类访问。
}
};
int main() {
B b(10);
cout << b.a << endl; // OK
// cout << b.a1 << endl; //错误,protected成员不能在类外访问。
// cout << b.a2 << endl; // 错误,类外不能访问protected成员
// cout << b.a3 << endl; // 错误,类外不能访问private成员
return 0;
}
private 继承:
#include <iostream>
#include <assert.h>
using namespace std;
class A {
public:
int a;
A() {
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void func() {
cout << a << endl; //正确
cout << a1 << endl; //正确
cout << a2 << endl; //正确
cout << a3 << endl; //正确
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B: private A {
public:
int a;
B(int i) {
A();
a = i;
}
void fun(){
cout << a << endl; //正确,public成员
cout << a1 << endl; //正确,基类public成员,在派生类中变成了private,可以被派生类访问。
cout << a2 << endl; //正确,基类的protected成员,在派生类中变成了private,可以被派生类访问
// cout << a3 << endl; //错误,基类的private成员不能被派生类访问。
}
};
int main() {
B b(10);
cout << b.a << endl; // OK
// cout << b.a1 << endl; ///错误,private成员不能在类外访问。
// cout << b.a2 << endl; //错误,private成员不能在类外访问。
// cout << b.a3 << endl; //错误,private成员不能在类外访问。
return 0;
}
1.4 类构造函数 & 析构函数
1.4.1 类的构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值:
#include <iostream>
using namespace std;
class Line {
public:
void setLength(double len);
double getLength(void);
Line(double len); // 构造函数
private:
double length;
};
Line::Line(double len) {
length = len;
cout << "Object is being created, length = " << length << endl;
}
void Line::setLength(double len) {
length = len;
}
double Line::getLength(void) {
return length;
}
int main() {
Line line(10);
// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <<endl;
line.setLength(6);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
也可以使用初始化列表在构造函数中来初始化字段,下面的写法效果与上面等价:
Line::Line(double len): length(len) {
cout << "Object is being created, length = " << length << endl;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,可以使用上面的语法,
C:C(double a, double b, double c): X(a), Y(b), Z(c) {
...
}
1.4.2 类的析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
#include <iostream>
using namespace std;
class Line {
public:
void setLength(double len);
double getLength(void);
Line(); // 构造函数
~Line();
private:
double length;
};
Line::Line(void) {
cout << "Object is being created" << endl;
}
Line::~Line(void) {
cout << "Object is being deleted" << endl;
}
void Line::setLength(double len) {
length = len;
}
double Line::getLength(void) {
return length;
}
int main() {
Line line;
line.setLength(6);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
输出:
Object is being created
Length of line : 6
Object is being deleted
1.5 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象。
- 复制对象把它作为参数传递给函数。
- 复制对象,并从函数返回这个对象。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
classname (const classname& obj) {
// 构造函数的主体
}
obj 是一个对象引用,该对象是用于初始化另一个对象的。
#include <iostream>
using namespace std;
class Line {
public:
int getLength(void);
Line(int len); // 简单的构造函数
Line(const Line& obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int* ptr;
};
Line::Line(int len) {
cout << "调用构造函数" << endl;
ptr = new int;
*ptr = len;
}
Line::Line(const Line& obj) {
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void) {
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength(void) {
return *ptr;
}
void display(Line obj) {
cout << "line 大小 : " << obj.getLength() <<endl;
}
int main() {
Line line(10);
Line line2 = line; // 这里调用了拷贝构造函数
display(line); // 复制对象把它作为参数传递给函数,触发拷贝构造函数
display(line2);
return 0;
}
输出:
调用构造函数 // 构造line触发构造函数
调用拷贝构造函数并为指针 ptr 分配内存 // Line line2 = line;通过使用另一个同类型的对象来初始化新创建的对象,触发拷贝构造函数
调用拷贝构造函数并为指针 ptr 分配内存 // display(line); // 复制对象把它作为参数传递给函数,触发拷贝构造函数
line 大小 : 10
释放内存 // display(line); 结束,释放函数参数拷贝的对象
调用拷贝构造函数并为指针 ptr 分配内存 // display(line2);// 复制对象把它作为参数传递给函数,触发拷贝构造函数
line 大小 : 10
释放内存 // // display(line2); 结束,释放函数参数拷贝的对象
释放内存 // 主函数结束,释放 line
释放内存 // 主函数结束,释放 line2
1.6 友元函数
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。友元函数的原型有在类的定义中出现,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:
class Box {
double width;
public:
double length;
friend void printWidthP(Box box);
void setWidth(double wid);
};
声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:
friend class ClassTwo;
1.6 内联函数
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
实际上,对于现代编译器而言,内联只是建议编译器把 inline 修饰的函数作为内联处理,最终是否内联,由编译器决定。
1.7 this 指针
在 C++ 中,this 指针是一个指向当前对象的指针实例。每一个对象都能通过 this 指针来访问自己的地址。
this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
成员函数通过 this 指针来访问成员变量,可以明确地告诉编译器我们想要访问当前对象的成员变量,而不是函数参数或局部变量。通过使用 this 指针,可以在成员函数中访问当前对象的成员变量,即使它们与函数参数或局部变量同名,这样可以避免命名冲突,并确保我们访问的是正确的变量。
#include <iostream>
using namespace std;
class Box {
public:
Box(double l=2.0, double b = 2.0, double h = 1.0) {
cout <<"调用构造函数。" << endl;
length = l;
breadth = b;
height = h;
}
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this -> Volume() > box.Volume();
}
private:
double length;
double breadth;
double height;
};
int main() {
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
if (Box1.compare(Box2)) {
cout << "Box2 的体积比 Box1 小" << endl;
} else {
cout << "Box2 的体积大于或等于 Box1" << endl;
}
return 0;
}
1.8 指向类的指针
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->
。
#include <iostream>
using namespace std;
class Box {
public:
Box(double l=2.0, double b = 2.0, double h = 1.0) {
cout <<"调用构造函数。" << endl;
length = l;
breadth = b;
height = h;
}
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this -> Volume() > box.Volume();
}
private:
double length;
double breadth;
double height;
};
int main() {
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
Box* ptr;
ptr = &Box1;
cout << "Volume of Box1: " << ptr->Volume() << endl;
ptr = &Box2;
cout << "Volume of Box2: " << ptr->Volume() << endl;
return 0;
}
1.9 类的静态成员
可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。静态成员在类的所有对象中是共享的。
如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 ::
来重新声明静态变量从而对它进行初始化,
#include <iostream>
using namespace std;
class Box {
public:
static int objCnt;
Box(double l=2.0, double b = 2.0, double h = 1.0) {
cout <<"调用构造函数。" << endl;
length = l;
breadth = b;
height = h;
objCnt += 1;
}
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this -> Volume() > box.Volume();
}
private:
double length;
double breadth;
double height;
};
// 初始化类 Box 的静态成员
int Box::objCnt = 0;
int main() {
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
cout << "Total objs: " << Box::objCnt << endl;
cout << "Total objs: " << Box1.objCnt << endl;
return 0;
}
静态成员函数
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 ::
就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。静态成员函数有一个类范围,他们不能访问类的 this 指针。可以使用静态成员函数来判断类的某些对象是否已被创建。
静态成员函数与普通成员函数的区别:
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)
- 这个可以这么理解:静态成员函数可以类对象不存在的情况被调用,此时由于没有具体的类对象,那么非静态成员变量和非静态成员函数还没有被创建,也没有指向类对象实例的 this,指针。
- 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针
#include <iostream>
using namespace std;
class Box {
public:
static int objCnt;
Box(double l=2.0, double b = 2.0, double h = 1.0) {
cout <<"调用构造函数。" << endl;
length = l;
breadth = b;
height = h;
objCnt += 1;
}
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this -> Volume() > box.Volume();
}
static int getCount(){
return objCnt;
}
private:
double length;
double breadth;
double height;
};
// 初始化类 Box 的静态成员
int Box::objCnt = 0;
int main() {
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
cout << "Final Stage Count: " << Box1.getCount() << endl;
return 0;
}
关于继承权限的实验:
#include <iostream>
/*
实验 class 的不同权限的继承对基类的成员变量的权限
这里定义四个 class,a 为基类,b,c,d 分别 public,protected,private 继承自 a
*/
class a {
public:
int apub = 1;
double getProtect() {
return this->apro;
}
float getPrivate() {
return this->apri;
}
protected:
double apro = 1.1;
private:
float apri = 2.2;
};
class b: public a {
public:
int bpub = 3;
double getCurrProtect() {
return this->bpro;
}
float getCurrPrivate() {
return this->bpri;
}
double getParProtect() {
return this->apro;
}
float getParPrivate() {
// 不可以直接访问 base class 的 private 成员变量
// return this->apri; // Error
return this->getPrivate();
}
protected:
double bpro = 4.4;
private:
float bpri = 5.5;
};
class c: protected a {
public:
int cpub = 6;
double getCurrProtect() {
return this->cpro;
}
float getCurrPrivate() {
return this->cpri;
}
int getParPublic() {
return this->apub;
}
double getParProtect() {
return this->apro;
}
float getParPrivate() {
// 不可以直接访问 base class 的 private 成员变量
// return this->apri; // Error
return this->getPrivate();
}
protected:
double cpro = 7.7;
private:
float cpri = 8.8;
};
class d: private a {
public:
int dpub = 9;
double getCurrProtect() {
return this->dpro;
}
float getCurrPrivate() {
return this->dpri;
}
int getParPublic() {
return this->apub;
}
double getParProtect() {
return this->apro;
}
float getParPrivate() {
// 不可以直接访问 base class 的 private 成员变量
// return this->apri; // Error
return getPrivate();
}
protected:
double dpro = 10.1;
private:
float dpri = 12.2;
};
int main() {
a aa;
b bb;
c cc;
d dd;
// base
std::cout << "\nbase class" << std::endl;
std::cout << "public val: " << aa.apub << std::endl;
// 也不可以直接使用类来访问成员变量
// std::cout << "public val: " << a::apub << std::endl; // Error
// 不可以在外部直接使用 class 的 protected 成员
// std::cout << "protected val: " << aa.apro << std::endl; // Error
// 需要使用 public 的成员函数间接访问
std::cout << "protected val: " << aa.getProtect() << std::endl;
// 不可以在外部直接使用 class 的 private 成员
// std::cout << "private val: " << aa.apro << std::endl; // Error
// 需要使用 public 的成员函数间接访问
std::cout << "private val: " << aa.getPrivate() << std::endl;
// public 继承
std::cout << "\npublic inheritance" << std::endl;
// public 继承的子类,在外部可以直接访问基类的 public 成员
std::cout << "parent public val: " << bb.apub << std::endl;
// public 继承的子类,在外部可以不可以直接访问基类的 protected 成员
// std::cout << "parent protected val: " << bb.apro << std::endl; // Error
std::cout << "parent protected val: " << bb.getParProtect() << std::endl;
// public 继承的子类,在外部可以不可以直接访问基类的 private 成员
// std::cout << "parent private val: " << bb.apri << std::endl; // Error
std::cout << "parent private val: " << bb.getParPrivate() << std::endl;
// protected 继承
std::cout << "\nprotected inheritance" << std::endl;
// protected 继承的子类,在外部不可以直接访问基类的 public 成员
// std::cout << "parent public val: " << cc.apub << std::endl; // Error
std::cout << "parent public val: " << cc.getParPublic() << std::endl;
// protected 继承的子类,在外部可以不可以直接访问基类的 protected 成员
// std::cout << "parent protected val: " << cc.apro << std::endl; // Error
std::cout << "parent protected val: " << cc.getParProtect() << std::endl;
// protected 继承的子类,在外部可以不可以直接访问基类的 private 成员
// std::cout << "parent private val: " << cc.apri << std::endl; // Error
std::cout << "parent private val: " << cc.getParPrivate() << std::endl;
// private 继承
std::cout << "\nprivate inheritance" << std::endl;
// private 继承的子类,在外部不可以直接访问基类的 public 成员
// std::cout << "parent public val: " << dd.apub << std::endl; // Error
std::cout << "parent public val: " << dd.getParPublic() << std::endl;
// private 继承的子类,在外部可以不可以直接访问基类的 protected 成员
// std::cout << "parent protected val: " << dd.apro << std::endl; // Error
std::cout << "parent protected val: " << dd.getParProtect() << std::endl;
// private 继承的子类,在外部可以不可以直接访问基类的 private 成员
// std::cout << "parent private val: " << dd.apri << std::endl; // Error
std::cout << "parent private val: " << dd.getParPrivate() << std::endl;
return 0;
}
结论:
-
基类的 private 成员变量在子类中不能直接访问
-
类的 private 成员变量在子类中不能直接访问,需要通过类的 public 方法来间接访问
- public 继承的子类,在外部可以直接访问基类的 public 成员
- 这个可以认为是 public 继承的子类,会将父类的 public 成员继承为自己的 public 成员变量,因此可以在外部直接访问
- public 继承的子类,在外部可以不可以直接访问基类的 protected 成员
- 这个可以认为是 public 继承的子类,会将父类的 protected 成员继承为自己的 protected 成员变量,因此不可以在外部直接访问
- protected 继承的子类,在外部不可以直接访问基类的 public 成员
- 这个可以认为是 protected 继承的子类,会将父类的 public 成员继承为自己的 protected 成员变量,因此不可以在外部直接访问
-
protected 继承的子类,在外部可以不可以直接访问基类的 protected 成员
-
protected 继承的子类,在外部可以不可以直接访问基类的 private 成员
- private 继承的子类,在外部不可以直接访问基类的 public 成员; private 继承的子类,在外部可以不可以直接访问基类的 protected 成员; private 继承的子类,在外部可以不可以直接访问基类的 private 成员
–>
-
基类的 private 成员变量是基类独有的,无论是在外部的调用,还是子类中,都无法直接使用,可以通过基类的 public 方法来间接访问
-
基类的 protect 成员是为子类继承设计的,即在外部调用无法直接使用,在子类中可以直接使用,在外部和子类中都可以通过基类的 public 方法来间接访问
-
基类的 public 成员变量是为外部访问设计的,在外部访问和子类中都可以直接使用,也可以通过基类的 public 方法来间接访问
-
子类继承基类的方式,决定了子类从基类获取到的成员的权限类型,并且基类的 private 成员,子类实际上是没有直接继承的。
2 继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,达到了重用代码功能和提高执行效率的效果。
当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog: public Animal {
// bark() 函数
};
2.1 基类 & 派生类
一个类可以派生自多个类,可以从多个基类继承数据和函数。定义一个派生类,使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
class derived_class: access_scifier base_class
访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
2.2 访问控制和继承
派生类可以访问基类中所有的非私有成员。
出不同的访问类型:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数
- 基类的重载运算符
- 基类的友元函数
2.3 继承类型
基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过访问修饰符 access-specifier 来指定的。
几乎不使用 protected
或 private
继承,通常使用 public
继承。当使用不同类型的继承时,遵循以下几个规则:
-
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
-
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
-
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
2.4 多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… {
<派生类类体>
};
访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,
#include <iostream>
// 基类 Shape
class Shape {
public:
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost {
public:
int getCost(int area) {
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost {
public:
int getArea() {
return (width * height);
}
};
int main() {
Rectangle rect;
int area;
rect.setWidth(5);
rect.setHeight(7);
area = rect.getArea();
std::cout << "Total area: " << area << std::endl;
std::cout << "Total paint cost: $" << rect.getCost(area) << std::endl;
return 0;
}
3 重载
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当调用一个重载函数或重载运算符时,编译器通过把使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
3.1 函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。
重载 print()
用来输出不同的数据类型:
#include <iostream>
class printData {
public:
void print(int i) {
std::cout << "INT: " << i << std::endl;
}
void print(double f) {
std::cout << "DOUBLE: " << f << std::endl;
}
void print(char c[]) {
std::cout << "STRING: " << c << std::endl;
}
};
int main() {
printData pd;
pd.print(5);
pd.print(2.1);
pd.print("Hello Cpp");
return 0;
}
3.2 运算符重载
可以重定义或重载大部分 C++ 内置的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
Box operator+(const Box&);
声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。
大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下所示:
Box operator+(const Box&, const Box&);
#include <iostream>
class Box {
private:
double length;
double heigth;
double breadth;
public:
double getV(void) {
return length * breadth * heigth;
}
void setLength(double len) {
length = len;
}
void setBreadth(double bread) {
breadth = bread;
}
void setHeigth(double heig) {
heigth = heig;
}
Box operator + (const Box& b) {
Box box;
// 这里的bxo加法定义合理性不要在意
box.length = this -> length + b.length;
box.heigth = this -> heigth + b.heigth;
box.breadth = this -> breadth + b.breadth;
return box;
}
};
int main() {
Box box1;
Box box2;
Box box3;
box1.setBreadth(1.1);
box1.setHeigth(2.2);
box1.setLength(3.3);
box2.setBreadth(4.4);
box2.setHeigth(5.5);
box2.setLength(6.6);
box3 = box1 + box2;
std::cout << "V1: " << box1.getV() << std::endl;
std::cout << "V2: " << box2.getV() << std::endl;
std::cout << "V3: " << box3.getV() << std::endl;
return 0;
}
3.3 可重载运算符/不可重载运算符
下面是可重载的运算符列表:
下面是不可重载的运算符列表:
.
:成员访问运算符.*
,->*
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符#
: 预处理符号
3.4 各种运算符重载
4 多态
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
在 C++ 中,多态(Polymorphism)是面向对象编程的重要特性之一。C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。
多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。
以下是多态的几个关键点:
-
虚函数(Virtual Functions):
- 在基类中声明一个函数为虚函数,使用关键字
virtual
。 - 派生类可以重写(override)这个虚函数。
- 调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。
- 在基类中声明一个函数为虚函数,使用关键字
-
动态绑定(Dynamic Binding):
- 也称为晚期绑定(Late Binding),在运行时确定函数调用的具体实现。
- 需要使用指向基类的指针或引用来调用虚函数,编译器在运行时根据对象的实际类型来决定调用哪个函数。
-
纯虚函数(Pure Virtual Functions):
- 一个包含纯虚函数的类被称为抽象类(Abstract Class),它不能被直接实例化。
- 纯虚函数没有函数体,声明时使用
= 0
。 - 它强制派生类提供具体的实现。
-
多态的实现机制:
- 虚函数表(V-Table):C++ 运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。
- 虚函数指针(V-Ptr):对象中包含一个指向该类虚函数表的指针。
-
使用多态的优势:
- 代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
- 扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
- 解耦:多态允许程序设计更加模块化,降低类之间的耦合度。
-
注意事项:
- 只有通过基类的指针或引用调用虚函数时,才会发生多态。
- 如果直接使用派生类的对象调用函数,那么调用的是派生类中的版本,而不是基类中的版本。
- 多态性需要运行时类型信息(RTTI),这可能会增加程序的开销。
一个多态的例子:
#include <iostream>
// 基类 Animal
class Animal {
public:
// 虚函数 sound,为不同的动物发声提供接口
virtual void sound() const {
std::cout << "Animal makes a sound" << std::endl;
}
// 虚析构函数确保子类对象被正确析构
virtual ~Animal() {
std::cout << "Animal destroyed" << std::endl;
}
};
// 派生类 Dog,继承自 Animal
class Dog: public Animal {
public:
// 重写 sound 方法
void sound() const override {
std::cout << "Dog barks" << std::endl;
}
~Dog() {
std::cout << "Dog destroyed" << std::endl;
}
};
// 派生类 Cat,继承自 Animal
class Cat: public Animal {
public:
// 重写 sound 方法
void sound() const override {
std::cout << "Cat meows" << std::endl;
}
// ~Cat() {
// std::cout << "Cat destroyed" << std::endl;
// }
};
int main() {
Animal* animalPtr; // 基类指针
// 创建 Dog 对象,并指向 Animal 指针
animalPtr = new Dog();
animalPtr->sound(); // 调用 Dog 的 sound 方法
delete animalPtr; // 释放内存,调用 Dog 和 Animal 的析构函数
// 创建 Cat 对象,并指向 Animal 指针
animalPtr = new Cat();
animalPtr->sound(); // 调用 Cat 的 sound 方法
delete animalPtr; // 释放内存,由于 cat 类没有重写析构函数,实际只调用 Animal 的析构函数
return 0;
}
输出:
Dog barks
Dog destroyed
Animal destroyed
Cat meows
Animal destroyed
说明:
- 基类 Animal:
- Animal 类定义了一个虚函数 sound(),这是一个虚函数(virtual),用于表示动物发声的行为。
- ~Animal() 为虚析构函数,确保在释放基类指针指向的派生类对象时能够正确调用派生类的析构函数,防止内存泄漏。
- 派生类 Dog 和 Cat:
- Dog 和 Cat 类都从 Animal 类派生,并各自实现了 sound() 方法。
- Dog 的 sound() 输出”Dog barks”;Cat 的 sound() 输出”Cat meows”。这使得同一个方法(sound())在不同的类中表现不同的行为。
- 主函数 main():
- 释放 Dog 对象时,先调用 Dog 的析构函数,再调用 Animal 的析构函数。
-
虚函数:通过在基类中使用 virtual 关键字声明虚函数,派生类可以重写这个函数,从而使得在运行时根据对象类型调用正确的函数。
-
动态绑定:C++ 的多态通过动态绑定实现。在运行时,基类指针 animalPtr 会根据它实际指向的对象类型(Dog 或 Cat)调用对应的 sound() 方法。
- 虚析构函数:在具有多态行为的基类中,析构函数应该声明为 virtual,以确保在删除派生类对象时调用派生类的析构函数,防止资源泄漏。
4.1 动态绑定
下面通过多态实现了一个通用的 Shape 基类和两个派生类 Rectangle 和 Triangle,并通过基类指针调用不同的派生类方法,展示了多态的动态绑定特性。
#include <iostream>
// 基类 Shape,表示形状
class Shape {
protected:
int width, height;
public:
// 构造函数,带有默认参数
Shape(int a = 0, int b = 0): width(a), height(b) {}
// 虚函数 area,用于计算面积
// 使用 virtual 关键字,实现多态
virtual int area() {
std::cout << "Shape class area: " << std::endl;
return 0;
}
};
// 派生类 Rectangle,表示矩形
class Rectangle: public Shape {
public:
// 构造函数,使用基类构造函数初始化 width 和 height
Rectangle(int a = 0, int b = 0) : Shape(a, b) {}
// 重写 area 函数,计算矩形面积
int area() override {
std::cout << "Rectangle class area: " << std::endl;
return width * height;
}
};
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
public:
// 构造函数,使用基类构造函数初始化 width 和 height
Triangle(int a = 0, int b = 0) : Shape(a, b) { }
// 重写 area 函数,计算三角形面积
int area() override {
std::cout << "Triangle class area: " << std::endl;
return (width * height / 2);
}
};
int main() {
Shape* shape; // 基类指针
Rectangle rec(10, 7);
Triangle tri(10, 5);
shape = &rec;
std::cout << "Rectangle Area: " << shape->area() << std::endl;
shape = &tri;
std::cout << "Triangle Area: " << shape->area() << std::endl;
return 0;
}
输出:
Rectangle Area: Rectangle class area:
70
Triangle Area: Triangle class area:
25
说明:
-
Shape 是一个抽象基类,定义了一个虚函数 area()。area() 是用来计算面积的虚函数,并使用了 virtual 关键字,这样在派生类中可以重写该函数,进而实现多态。
-
width 和 height 是 Shape 的 protected 属性,只能在 Shape 类及其派生类中访问。
-
Rectangle 继承了 Shape 类,并重写了 area() 方法,计算矩形的面积。area() 方法使用了 override 关键字,表示这是对基类 Shape 的 area() 方法的重写。Triangle 类也继承自 Shape,并重写了 area() 方法,用于计算三角形的面积。
-
main 函数中,首先将 shape 指针指向 Rectangle 对象 rec,然后调用 shape->area()。由于 area() 是虚函数,此时会动态绑定到 Rectangle::area(),输出矩形的面积。接着,将 shape 指针指向 Triangle 对象 tri,调用 shape->area() 时会动态绑定到 Triangle::area(),输出三角形的面积。
4.2 虚函数
虚函数是在基类中使用关键字 virtual 声明的函数。虚函数允许子类重写它,从而在运行时通过基类指针或引用调用子类的重写版本,实现动态绑定。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
特点:
-
在基类中可以有实现。通常虚函数在基类中提供默认实现,但子类可以选择重写。
-
动态绑定:在运行时根据对象的实际类型调用相应的函数版本。
-
可选重写:派生类可以选择性地重写虚函数,但不是必须。
4.3 纯虚函数
纯虚函数是没有实现的虚函数,在基类中用 = 0
来声明。纯虚函数表示基类定义了一个接口,但具体实现由派生类负责。纯虚函数使得基类变为抽象类(abstract class),无法实例化。
特点:
- 必须在基类中声明为
= 0
,表示没有实现,子类必须重写。 - 抽象类:包含纯虚函数的类不能直接实例化,必须通过派生类实现所有纯虚函数才能创建对象。
- 接口定义:纯虚函数通常用于定义接口,让派生类实现具体行为。
= 0
告诉编译器,函数没有主体。
#include <iostream>
using namespace std;
class Shape {
public:
virtual int area() = 0; // 纯虚函数,强制子类实现此方法
};
class Rectangle : public Shape {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) { }
int area() override { // 实现纯虚函数
return width * height;
}
};
5 数据抽象
数据抽象是指只向外界提供关键信息,并隐藏其后台的实现细节,只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法。例如,程序可以调用 sort()
函数,而不需要知道函数中排序数据所用到的算法。实际上,函数排序的底层实现会因库的版本不同而有所差异,只要接口不变,函数调用就可以照常工作。
在 C++ 中,使用类来定义自己的抽象数据类型(ADT)。可以使用类 iostream 的 cout 对象来输出数据到标准输出,如下所示:
#include <iostream>
using namespace std;
int main() {
cout << "Hello C++" <<endl;
return 0;
}
5.1 访问标签强制抽象
在 C++ 中,使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签:
- 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
- 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。
访问标签出现的频率没有限制。每个访问标签指定了紧随其后的成员定义的访问级别。指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。
5.2 数据抽象的好处
数据抽象有两个重要的优势:
- 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
- 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。
如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。
C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例:
#include <iostream>
class Addr {
private:
// 对外隐藏的数据
int total;
public:
Addr(int i = 0) {
total = i;
}
// 对外的接口
void addNum(int num) {
total += num;
}
// 对外的接口
int getTotal() {
return total;
}
};
int main() {
Addr a;
a.addNum(10);
a.addNum(20);
std::cout << "Total: " << a.getTotal() << std::endl;
return 0;
}
公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是用户不需要了解的,但又是类能正常工作所必需的。
抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。
6 数据封装
数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。
所有的 C++ 程序都有以下两个基本要素:
- 程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
- 程序数据:数据是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。
通过访问修饰符 public、protected、private 来控制数据的访问权限,是实现封装的一种方式。把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。
C++ 程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。数据封装通过类和访问修饰符(public, private, protected)来实现,
#include <iostream>
class Student {
private:
std::string name;
int age;
public:
Student(std::string n, int a) {
name = n;
age = a;
}
// 访问器函数
std::string getName() {
return name;
}
int getAge() {
return age;
}
// 修改器函数
void setName(std::string n) {
name = n;
}
void setAge(int a) {
if (a > 0) {
age = a;
} else {
std::cout << "Invalid ags!" << std::endl;
}
}
void printInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
Student s1("Alice", 10);
s1.printInfo();
s1.setName("Bob");
s1.setAge(15);
s1.printInfo();
return 0;
}
通常情况下,都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。
这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
7 接口(抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现。C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
#include <iostream>
// 基类
class Shape {
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape {
public:
int getArea() {
return (width * height);
}
};
class Triangle: public Shape {
public:
int getArea() {
return (width * height)/2;
}
};
int main() {
Rectangle rect;
Triangle tria;
rect.setWidth(5);
rect.setHeight(7);
std::cout << "Total Rectangle area: " << rect.getArea() << std::endl;
tria.setWidth(5);
tria.setHeight(7);
std::cout << "Total Triangle area: " << tria.getArea() << std::endl;
return 0;
}
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
定义虚函数是为了允许用基类的指针来调用子类的这个函数。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。