面向对象
面向对象是c++
区别于c
的重要特性。类的思想由改进结构体的功能诞生。
- 结构体 多个数据的集合,属于用户自定义类型
- 类 多个数据和函数的集合,属于用户自定义类型
- 成员 类中的数据或函数
- 对象 类生成的实例
定义
class ClassName
{
// ...
}
成员变量
定义在类中的变量称为成员变量。
成员函数
定义
类的成员函数既可以定义在类中也可以定义在类外。
// animal.h
class Animal
{
public:
void greeting(void) { cout << "Hello!" << endl; }
void eat(void);
}
// animal.cpp
void Animal::eat(void)
{
cout << "eating" << endl;
}
定义在类内的函数会默认inline
,因此只有比较短小的函数建议定义在类内。
构造函数
类的构造函数会在每次对象被创建时调用,目的是初始化对象。
class Animal
{
public:
Animal(void);
}
Animal::Animal(void)
{
cout << "Animal created." << endl;
}
可以看出,构造函数与类同名,且没有返回值。
默认构造函数
如果用户没有显式的声明一个构造函数,编译器会提供一个默认的构造函数。这个构造函数没有参数,同时也没有任何操作。一旦定义了构造函数,默认构造函数就失效了。
形式上,默认构造函数等价于下面定义的构造函数
class Animal
{
public:
Animal(void) {}
}
但严格来讲,上面定义的构造函数只是一个无参无函数体的函数,而不是默认构造函数。因为默认构造函数是不能显式写出的。
提问:对象构造时如何调用默认构造函数?
Animal a; // 调用默认构造
Animal a(); // 函数声明
初始化列表
构造函数用于为类中的成员变量初始化,因此常常传入用于初始化的值作为参数。这时,我们可以使用初始化列表
class Animal
{
int age;
public:
Animal(int age);
}
Animal::Animal(int age) :
age(age)
{
// 等价于
// this->age = age;
}
初始化列表的赋值顺序是与成员变量在类中的声明顺序一致的。
// animal.h
#pragma once
class Animal {
int c;
int d;
public:
Animal(int a, int b);
};
// animal.cpp
Animal::Animal(int a, int b) :
d(a + b),
c(d + a)
{
cout << "c: " << c << endl;
cout << "d: " << d << endl;
}
void main(int argc, char* argv[]) {
Animal a(3, 5);
return;
}
/* output:
* c: -858993457
* d: 8
*/
成员c
在初始化时调用的d
实际上还没有被正确初始化,因此被赋值为乱码。
拷贝构造
拷贝构造函数是一类特殊的构造函数。
定义
class Animal
{
public:
Animal(const Animal &other);
}
Animal::Animal(const Animal &other)
{
cout << "copy" << endl;
}
这里我们显式的定义了一个Animal
类的拷贝构造函数。如果没有定义,编译器会自动提供一个默认的拷贝构造函数。这一点和默认构造类似。
浅拷贝与深拷贝
对于自定义类型的指针,拷贝存在深浅之分。浅拷贝指的是将指针拷贝过来,也就是说新旧变量指向同一片内存空间。而深拷贝则是将内存空间复制一份,然后让新的变量指向新的地址空间。
class Animal
{
int *age;
public:
Animal(const Animal &other)
{
this->age = other.age; // 浅拷贝
this->age = new int(*(other.age)); // 深拷贝
}
}
上面的例子是一个错误的使用了new
这个操作符,但是正确的说明了深拷贝与浅拷贝的区别。如果使用浅拷贝,将会导致多个指针指向同一变量,容易造成隐蔽的错误,因此需要极力避免。
由于默认的拷贝构造函数只是将对象的成员依次赋值,因此完成的是浅拷贝。要完成深拷贝,就需要我们自己定义拷贝构造函数。
调用情景
拷贝构造的调用情景比较多。一旦程序中出现了这些情景,就需要考虑显式定义拷贝构造函数了。为了便于说明,我们定义Animal
类如下
class Animal
{
public:
Animal(const Animal &other)
{
cout << "animal duplicated" << endl;
}
}
这样,只要拷贝构造被调用,我们可以从输出看出。
拷贝初始化
Animal animal;
Animal other = animal; // 拷贝初始化
函数参数传值
本质上参数传值就是拷贝初始化的过程。
void fun(Animal animal) // Animal animal = 0($a0)
{
// ...
}
void main(void)
{
Animal animal;
fun(animal);
}
这里再次强调
Never Pass By Value!
函数返回对象
Animal fun(void)
{
Animal *animal = new Animal;
return *animal;
}
有趣的是,不管函数的返回值有没有被保存,拷贝构造都只会调用一次。
void main(void)
{
fun();
Animal animal = fun();
}
转换构造
类型转换用于强制将某种类型的对象转换为另一种类型。如果要在自定义类型中增加类型转换的功能,可以借助于转换构造函数。
class Animal
{
int age;
public:
Animal(int age) : age(age) {}
}
void main()
{
Animal animal = 3;
}
以上写法可以视作Animal
类的构造函数强制将3
转换成Animal
类的对象。如果构造函数中只有一个参数,这个构造函数大概率会被识别为转换构造函数。如果不希望这样,可以在函数前标记explicit
class Animal
{
int age;
public:
explicit Animal(int age) : age(age) {}
}
void main()
{
// Animal animal = 3;
// error C2440: 'initializing': cannot convert from 'int' to 'Animal'
}
析构函数
析构函数用于对象的删除。他也与类同名,只是在最前面多了一个~
。此外,析构函数没有返回值和参数,需要注意构造函数可以有参数。析构函数的存在可以确保对象中的资源在对象删除前被合理释放。
class Animal
{
int *age;
public:
Animal(void) { age = new int(1); }
~Animal(void) { delete age; }
}
如果没有定义析构函数,则一旦删除Animal
对象,其内部的age
指向的内存空间将永远无法得到释放。这样的情况被称为内存泄漏。