面向对象的程序设计
面向对象的程序设计方法:
- 将某类客观事物共同特点 (属性) 归纳出来, 形成一个数据结构 (可以用多个变量描述事物的属性);
- 将这类事物所能进行的行为也归纳出来, 形成一个个函数, 这些函数可以用来操作数据结构.
面向对象的特点有 抽象, 封装, 继承, 多态.
一般来说, 对象所占用的内存空间的大小, 等于所有成员变量的大小之和.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class CRectangle {
public:
int w, h;
int Area() { return w * h; }
int Perimeter() { return 2 * (w + h); }
void Init(int w_, int h_) {
w = w_;
h = h_;
}
}; // 必须有分号
int main() {
int w, h;
CRectangle r; // r 是一个对象
cin >> w >> h;
r.Init(w, h);
cout << r.Area() << endl << r.Perimeter();
return 0;
}
|
和结构变量一样, 对象之间可以用 =
进行赋值, 但是不能用 ==
, !=
, >
, <
, >=
, <=
进行比较, 除非这些运算符经过了 重载.
1
2
3
|
CRectangle r1, r2;
r1.w = 5;
r2.Init(5, 4);
|
1
2
3
4
5
|
CRectangle r1, r2;
CRectangle *p1 = &r1;
CRectangle *p2 = &r2;
p1->w = 5;
p2->Init(5, 4);
|
1
2
3
|
CRectangle r1;
CRectangle &rr = r1;
rr.w = 5;
|
引用
引用名是对象名的别名, 指向同一个对象. 语法: 类名 &引用名 = 对象名; 引用的好处是可以减少指针的使用, 使得代码更加简洁.
1
2
3
4
5
6
7
8
9
|
// 要用指针, 否则参数传递会产生拷贝
void swap(int *a, int *b) {
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int n1, n2;
swap(&n1, &n2);
|
可以改写为:
1
2
3
4
5
6
7
8
|
void swap(int &a, int &b) {
int tmp;
tmp = a;
a = b;
b = tmp;
}
int n1, n2;
swap(n1, n2);
|
引用还可以作为函数的返回值.
1
2
3
4
5
6
|
int &ref(int &a) {
return a;
}
int n = 5;
ref(n) = 6; // n = 6
|
常量, 常引用, 常量指针
定义引用时, 前面加 const
关键字, 表示 常引用. 不能通过常引用修改对象的值, 即只读引用.
1
2
3
4
|
int n;
const int &r = n;
r = 5; // 错误
n = 5; // 正确
|
const T&
和 T&
是不同的类型. T&
类型的引用或 T
类型的变量可以用来初始化 const T&
类型的引用.
const T
类型的常变量和 const T&
类型的引用则不能用来初始化 T&
类型的引用, 除非进行强制类型转换.
不可通过常量指针修改其指向的内容, 但可以修改指针的指向 (引用不可以).
1
2
3
4
5
|
int n, m;
const int *p = &n;
*p = 5; // 错误
n = 5; // 正确
p = &m; // 正确
|
函数参数为常量指针时, 可避免函数内部不小心改变参数指针所指地方的内.
1
2
3
4
|
void myPrintf(const int *p) {
*p = 5; // 错误
p = &m; // 正确
}
|
类成员的访问控制
public
: 公有成员, 可以在类的外部访问.
private
: 私有成员, 只能在类的内部访问, 缺省默认为 private
.
protected
: 保护成员, 只能在类的内部和派生类中访问.
1
2
3
4
5
6
7
8
9
|
class className {
// 这三个关键字可以出现多次, 没有顺序要求
private:
// 私有属性和函数
public:
// 公有属性和函数
protected:
// 保护属性和函数
};
|
类内部可以访问当前对象和同类其他对象的私有成员.
“隐藏” 的目的是强制对成员变量的访问一定要通过成员函数进行, 那么以后成员变量的类
型等属性修改后, 只需要更改成员函数即可.
struct
和 class
的唯一区别是默认的访问控制权限不同, struct
默认为 public
, class
默认为 private
.
函数重载和缺省参数
函数名相同, 参数个数或类型不同, 注意没有返回值类型不同.
1
2
3
|
double _max(double f1, double f2);
int _max(int n1, int n2);
int _max(int n1, int n2, int n3);
|
C++ 中, 定义函数的时候可以让 最右边 的连续若干个参数有缺省值, 那么调用函数的时候, 若相应位置不写参数, 参数就是缺省值.
1
2
3
4
|
void func(int a, int b = 0, int c = 0);
func(1); // a = 1, b = 0, c = 0
func(1, 2); // a = 1, b = 2, c = 0
func(1, , 8); // 错误
|
成员函数也可以重载或有缺省参数.
构造函数
构造函数是一种特殊的成员函数: 名字与类名相同, 没有返回值 (void
也不行). 作用是初始化对象的数据成员.
如果定义类时没有定义构造函数, 编译器会生成一个默认的无参构造函数.
如果定义了, 默认构造函数就不会生成. 对象生成时, 构造函数自动调用. 生成之后不能再执行构造函数. 一个类可以有多个构造函数, 可以重载.
1
2
3
4
5
6
7
8
9
10
11
|
class Complex {
private:
double real;
double imag;
public:
void Set(double r, double i);
}; //编译器自动生成默认构造函数
// 两种写法均可.
Complex c1;
Complex *pc = new Complex;
|
手动加入构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i = 0) { // 带缺省参数
real = r;
imag = i;
}
};
Complex *pc1 = new Complex; // error, 没有参数
Complex c2(2); // OK
Complex *pc2 = new Complex(3, 4);
|
构造当然也可以 private
, 这样就不能用来生成对象, 但是可以用来实现单例模式.
构造函数还可以用在数组.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class CSample {
int x;
public:
CSample() {}
CSample(int n) { x = n; }
CSample(int m, int n) { x = m + n; }
};
int main() {
CSample array1[2]; // 两次无参
CSample array2[2] = {4, CSample(5, 3)}; // 两次有参
CSample array3[2] = {3}; // 一次有参一次无参
CSample *array4 = new CSample[2];
delete[] array4;
return 0;
}
|
复制构造函数只有一个参数, 且参数是本类的引用 (或常量引用). 如果没有定义, 编译器会生成一个默认的复制构造函数. 如果定义了, 默认复制构造函数就不会生成.
1
2
3
4
5
6
7
8
|
class Complex {
private:
double real;
double imag;
};
Complex c1;
Complex c2(c1); // 默认复制构造函数
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Complex {
public:
double real;
double imag;
Complex() {} // 必须要写, 否则编译器不会生成默认构造函数
Complex(const Complex &c) {
real = c.real;
imag = c.imag;
cout << "Copy Constructor called";
}
};
Complex c1;
Complex c2(c1); // 自定义复制构造函数
|
复制构造函数的调用时机:
-
用一个对象去初始化另一个对象.
1
2
|
Complex c1;
Complex c2(c1);
|
-
一个对象作为函数参数传递给一个非引用类型的参数.
1
2
3
|
void func(Complex c);
Complex c1;
func(c1);
|
-
一个对象作为函数返回值返回.
1
2
|
Complex func();
Complex c1 = func();
|
注意: 对象之间的赋值操作, 不会调用复制构造函数.
1
2
|
Complex c1, c2;
c1 = c2; // 不会调用复制构造函数
|
考虑到对象作为函数参数会掉用复制构造函数, 为了避免不必要的开销, 可以使用引用传递.
手动写复制构造函数的目的一般是为了实现深拷贝.
转换构造函数的目的是实现类型的自动转换. 不以说明符 explicit
声明 {且可以用单个参数调用 (C++11 前)} 的构造函数被称为转换构造函数.
1
2
3
4
5
6
7
8
9
10
11
12
|
class Complex {
public:
double real, imag;
Complex(int i) { // 类型转换构造函数
real = i;
imag = 0;
}
};
int main() {
Complex c1 = 9; // 隐式调用, 转换成一个临时 Complex 对象
return 0;
}
|
如果加了 explicit
关键字, 则只能显式调用.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Complex {
public:
double real, imag;
explicit Complex(int i) { // 类型转换构造函数
real = i;
imag = 0;
}
};
int main() {
Complex c1 = 9; // 错误
Complex c2(9); // 正确
return 0;
}
|
析构函数
析构函数是类的一个特殊成员函数, 名字由波浪号 ~
加类名构成, 没有参数, 也没有返回值, 对象消亡时即自动被调用, 作用是释放对象所占用的资源.
如果定义类时没写析构函数, 则编译器生成缺省析构函数.缺省析构函数什么也不做. 如果定义了析构函数, 缺省析构函数就不会生成. 一个类只有一个析构函数, 不能重载.
在数组生命周期结束时, 编译器会自动调用数组中每个元素的析构函数. delete
一个对象时, 会调用对象的析构函数. (注意, new
数组要用 delete[]
)
1
2
3
4
5
|
Ctest *pTest;
pTest = new Ctest; // 构造函数调用
delete pTest; // 析构函数调用
pTest = new Ctest[3]; // 构造函数调用 3 次
delete[] pTest; // 析构函数调用 3 次
|
this 指针
this
是一个指向对象本身的指针. 把 car.foo()
翻译成 C 就是 foo(&car)
1
2
3
4
5
6
7
8
9
|
class A {
int i;
public:
void hello() { cout << "hello" << endl; }
};
int main() {
A *p = NULL;
p->hello();
}
|
能运行且输出, 但是是未定义行为. 而且一旦 hello()
中用到了 this
, 就会出错.
非静态成员函数中可以直接使用 this
来代表指向该函数作用的对象的指针.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Complex {
public:
double real, imag;
void print() {
cout << real << "," << imag;
}
Complex(double r, double i) : real(r), imag(i) {}
Complex addOne() {
this->real++; // 等价于 real++;
this->print(); // 等价于 print()
return *this; // 返回对象本身
}
};
|
静态成员
静态成员即加了 static
关键字的成员.
静态成员变量是类的所有对象共享的. sizeof
不包括静态成员变量. 本质是全局变量. 静态成员函数是类的所有对象共享的函数, 静态成员函数只能访问静态成员变量和静态成员函数, 不能访问普通成员变量和普通成员函数, 也不可以用 this
指针. 本质是全局函数.
访问静态成员可以不通过对象访问. 类名::静态成员名.
1
2
|
int Rectangle::edges = 4;
Rectangle::printTotal();
|
即使类的对象不存在, 静态成员变量也存在.
成员对象和封闭类
有成员对象的类叫封闭类.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class CTyre {
private:
int radius;
int width;
public:
CTyre(int r, int w) : radius(r), width(w) {}
};
class CEngine {};
class CCar {
private:
int price; // 价格
CTyre tyre;
CEngine engine;
public:
CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int w) : price(p), tyre(tr, w) {}
|
这个例子 CCar
必须有构造函数, 因为 CTyre
和没有默认构造函数.
- 封闭类对象生成时, 先执行所有对象成员的构造函数, 然后才执行封闭类的构造函数.
- 对象成员的构造函数调用次序和对象成员在类中的说明次序一致, 与它们在成员初始化列表中出现的次序无关.
- 当封闭类的对象消亡时, 先执行封闭类的析构函数, 然后再执行成员对象的析构函数. 次序和构造函数的调用次序相反.
- 封闭类的对象, 如果是用默认复制构造函数初始化的, 那么它里面包含的成员对象也会用复制构造函数初始化.
友元
友元分为友元函数和友元类两种.
友元函数: 一个类的友元函数可以访问该类的私有成员.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class CCar; // 提前声明 CCar 类, 以便后面的 CDriver 类使用
class CDriver {
public:
void modifyCar(CCar *pCar);
};
class CCar {
private:
int price;
friend int mostExpensiveCar(CCar cars[], int total); // 声明友元
friend void CDriver::modifyCar(CCar *pCar); // 声明友元
// 可以将一个类的成员函数 (包括构造/析构函数) 说明为另一个类的友元。
};
void CDriver::modifyCar(CCar *pCar) {
pCar->price += 1000;
}
int mostExpensiveCar(CCar cars[], int total) {
int tmpMax = -1;
for (int i = 0; i < total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
|
如果 A 是 B 的友元类, 那么 A 的成员函数可以访问 B 的私有成员.
1
2
3
4
5
6
7
8
9
10
11
12
|
class CCar {
private:
int price;
friend class CDriver; // 声明 CDriver 为友元类
};
class CDriver {
public:
CCar myCar;
void modifyCar() {
myCar.price += 1000; // 因 CDriver 是 CCar 的友元类, 故此处可以访问其私有成员
}
};
|
友元类之间的关系不能传递, 不能继承.
常量对象, 常量成员函数
如果不希望某个对象的值被改变, 则定义该对象的时候可以在前面加 const
关键字变为常量对象.
在类的成员函数说明后面可以加 const
关键字, 则该成员函数成为常量成员函数. 常量成员函数内部不能改变属性的值, 也不能调用非常量成员函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Sample {
private:
int value;
public:
void func() {};
Sample() {}
void SetValue() const {
value = 0; // wrong
func(); // wrong
}
};
const Sample Obj;
Obj.SetValue(); // 常量对象上可以使用常量成员函数
|
对于
1
2
3
4
5
6
|
int getValue() const {
return n;
}
int getValue() {
return 2 * n;
}
|
两个函数, 名字和参数表都一样, 但是一个是 const
, 一个不是, 算重载.
加上 mutable
关键字的成员变量, 即使在常量成员函数中也可以被修改.
1
2
3
4
5
6
7
8
9
10
|
class CTest {
public:
bool getData() const {
m_n1++;
return m_b2;
}
private:
mutable int m_n1;
bool m_b2;
};
|