C++学习笔记4——面向对象

面向对象

面向对象是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指向的内存空间将永远无法得到释放。这样的情况被称为内存泄漏

猜你喜欢

转载自blog.csdn.net/LutingWang/article/details/89818325