【C++】类与对象

文章目录

  • [1. 面向过程与面向对象](#1. 面向过程与面向对象)
  • [2. 类(class)](#2. 类(class))
  • [3. 访问限定符](#3. 访问限定符)
  • [4. 类的实例化](#4. 类的实例化)
  • [5. this指针](#5. this指针)
  • [6. 默认成员函数](#6. 默认成员函数)
    • [6.1 构造函数](#6.1 构造函数)
    • [6.2 析构函数](#6.2 析构函数)
    • [6.3 拷贝构造函数](#6.3 拷贝构造函数)

1. 面向过程与面向对象

C语言是面向过程(procedure-oriented)的语言,分析出求解问题的步骤,通过函数调用逐步解决问题。以洗衣服举例,需要很多个步骤:

C++是面向对象(object-oriented)的语言,将一件事情拆分成不同的对象,靠对象之间的交互完成。洗衣服这件事可以拆分成三个对象:人、衣服和洗衣粉,可能也有洗衣机,洗衣服这个过程是由这三个或四个对象之间交互完成的。

2. 类(class)

C语言结构体中只能定义变量,在C++中结构体内不仅可以定义变量,也可以定义函数。

cpp 复制代码
struct Person {
	char name[20];
	int age;
	char gender;
	int height;
	int weight;
	char introduction[100];

	void showInfo() {
		cout << name << " - " << age << " - " << gender << endl;
	}

	void sleep() {
		
	}

	void washCloth() {

	}

	void readBook() {

	}

	void work() {

	}

	void study() {

	}
};

可以这么做是因为C++需要兼容C,事实上C++更喜欢用class关键字表示一个类:

cpp 复制代码
class Person
{
	// 类体由变量和函数组成
};

类中的变量称为类的属性或成员变量 ,类中的函数称为类的方法或成员函数 。可以将成员的声明和定义一起放在类中 ,如果成员函数放在类中定义,编译器可能会将其当成内联函数处理;也可以将类成员的声明和定义分开,类中的成员变量和成员函数的声明放在.h头文件,成员函数的定义放在.cpp文件。

类的作用域

类定义了一个新的作用域,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

cpp 复制代码
class Person {
	char name[20];
	int age;
	char gender;
	int height;
	int weight;
	char introduction[100];

	void showInfo();
	void sleep();
	void washCloth();
	void readBook();
	void work();
	void study();
};

void Person::showInfo() {
	cout << name << " - " << age << " - " << gender << endl;
}

3. 访问限定符

访问限定符用于确定类成员的访问权限

  1. public:被public修饰的成员在类外可以被访问;
  2. protected:被protected修饰的成员在类外不能被访问,但可以在继承的子类中被访问;
  3. private:被private修饰的成员在类外不能被访问。

class关键字定义类的默认访问权限是private,这是因为面向对象三大特性之一的封装。struct关键字定义类的默认访问权限是public,因为需要兼容C。

封装

面向对象的三大特性:封装、继承、多态。封装是将数据和操作数据的方法进行结合,通过访问权限来隐藏对象内部的属性和实现细节,控制哪些函数可以在类外部直接被使用,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。比如对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互。对于计算机使用者而言,不用关心内部核心部件,主板上线路是如何布局的,CPU内部是如何设计的。

cpp 复制代码
class Person {

private:
	char name[20];
	int age;
	char gender;
	int height;
	int weight;
	char introduction[100];

public:
	void showInfo();
	void sleep();
	void washCloth();
	void readBook();
	void work();
	void study();
};

void Person::showInfo() {
	cout << name << " - " << age << " - " << gender << endl;
}

4. 类的实例化

用类类型创建对象的过程,称为类的实例化。类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类成员变量。

cpp 复制代码
int main() {
	Person p1;
	Person p2;
	Person p3;
}

计算对象的内存大小与计算结构体的大小一致,可以看这篇文章计算结构体的大小了解,简单地说就是按计算结构体大小的方式来计算类成员变量的大小。成员函数不会包括在内,因为成员函数是n个对象共用的,存放在公共代码区。另外空类比较特殊,它也有大小,占用1个字节空间。

5. this指针

this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给

this形参,因此this指针并不存放在对象中,而是在栈中(可能也在寄存器中,取决于编译器)。

this指针的类型:类的类型* const,只能在"成员函数"的内部使用。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要程序员手动传递。

下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

cpp 复制代码
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	 A* p = nullptr;
	 p->Print();
	 return 0;
}

C.正常运行,表面存在空指针问题,但成员函数中并没有使用其它成员,仅仅只是打印一个字符串,不会导致空指针访问。

cpp 复制代码
class A
{ 
public:
    void PrintA() 
	{
	    cout<<_a<<endl;
	}
private:
	int _a;
};
int main()
{
    A* p = nullptr;
    p->PrintA();
    return 0;
}

B.运行崩溃,成员函数中使用了成员变量,实际上是this指针访问的成员,造成了空指针。

6. 默认成员函数

如果一个类中什么成员都没有,简称为空类。空类并不是什么都没有,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

1. 构造函数:初始化对象;
2. 析构函数:清理对象中申请的动态内存;
3. 拷贝构造:使用同类对象创建另一个对象;
4. 赋值重载:把一个对象赋值给另一个对象;

  1. 普通对象取地址重载;

  2. const对象取地址重载。

后面两个取地址重载很少用。

6.1 构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,并不是开空间创建对象,而是完成对象的初始化工作。

cpp 复制代码
class Date {
private:
	int year;
	int month;
	int day;
public:
	Date() { // 无参构造

	}
	Date(int y, int m, int d) {
		year = y;
		month = m;
		day = d;
	}
};

int main() {
	Date date1;
	Date date2(2024, 3, 11);
	return 0;
}

如果通过无参构造函数创建对象,后面不用跟括号,否则就成了函数声明。

构造函数特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 无参的构造函数、全缺省的构造函数、编译器默认生成的构造函数都称为默认构造函数,在一个类中这三个默认构造函数只能存在其中一个,因为这三个都可以不用传参,如果同时存在会产生调用歧义。
cpp 复制代码
class Date {
private:
	int year;
	int month;
	int day;
public:
	Date() { // 与下面全缺省的构造函数存在冲突

	}
	Date(int y = 2024, int m = 2, int d = 22) {
		year = y;
		month = m;
		day = d;
	}
};
  1. 编译器生成默认的构造函数,会对类中其它自定类型成员调用的它的默认构造
    函数。比如Date类中如果包含一个Time类成员:
cpp 复制代码
class Time {
private:
	int hour;
	int minute;
	int second;
public:
	Time() {
		cout << "Time()" << endl;
	}
};
class Date {
private:
	int year;
	int month;
	int day;
	Time time;
public:
	Date(int y = 2024, int m = 2, int d = 22) {
		cout << "Date(int, int, int)" << endl;
		year = y;
		month = m;
		day = d;
	}
};

int main() {
	Date date;
	return 0;
}

由于默认生成的构造函数不会进行有效的初始化,给的是随机值,所以C++11开始可以给内置类型(int/char/double等)成员在类中声明时给默认值,如果没有指定初始化则初始化成默认值。

cpp 复制代码
class Date
{
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	Time _time;
};

6.2 析构函数

析构函数不是完成对对象本身的销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数的特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数和无返回值类型。
  3. 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
  4. 析构函数不能重载。
  5. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
cpp 复制代码
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("Stack malloc failed.");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
  1. 编译器生成的默认析构函数,对自定义类型的成员调用它的析构函数。
cpp 复制代码
class Time
{
public:
 ~Time()
 {
 	cout << "~Time()" << endl;
 }
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	Time _time;
};
int main()
{
	Date d;
	return 0;
}
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。有资源申请时(malloc),一定要写,否则会造成资源泄漏。

6.3 拷贝构造函数

拷贝构造函数创建一个与已存在对象一样值的新对象。参数只有单个形参,该形参是对本类类型对象的引用,且一般常用const修饰,在用已存在的类型对象创建新对象时由编译器自动调用。

拷贝构造函数特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
cpp 复制代码
class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
	 _year = year;
	 _month = month;
	 _day = day;
 }
 Date(const Date& d)  
 {
	 _year = d._year;
	 _month = d._month;
	 _day = d._day;
 }
private:
	int _year;
	int _month;
	int _day;
};
  1. 拷贝构造函数的参数只有一个且必须是类类型对象的引用 ,使用传值方式的话编译器直接报错,因为会引发无穷递归调用。这是错误写法,传值会引发无穷递归:
cpp 复制代码
 Date(const Date d)  
 {
	 _year = d._year;
	 _month = d._month;
	 _day = d._day;
 }

原因如下:

cpp 复制代码
void Test1(const Date d) {

}
void Test2(const Date& d) {

}
int main() {
	Date date1;
	Test1(date1); // 调用Test1传值会首先调用拷贝构造
	Test2(date1); // 传引用不会去调用拷贝构造
}

所以说拷贝构造的形参不使用引用,会引发无穷递归:

初始化date2时传值而不传引用,调用了一层拷贝构造后,拷贝构造本身继续传值继续调用拷贝构造引发无穷递归,现在这样写的话vs直接爆红,编译都不给编译。

  1. 若未显式定义拷贝构造,默认的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝) 。如果类中有需要申请内存资源的成员(需要malloc的成员),默认的拷贝构造无法完成拷贝,需要自己显示定义完成深拷贝。比如:
cpp 复制代码
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("Stack malloc failed.");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

使用默认的拷贝构造传入stack1初始化stack2,stack2的array仅仅只是把stack1的array地址值拷贝过来了,意味着共用同一块内存。

显示定义拷贝构造为深拷贝的正确写法:

cpp 复制代码
	Stack(const Stack& rStack) {
		_capacity = rStack._capacity;
		_size = rStack._size;
		_array = (DataType*)malloc(sizeof(DataType) * _capacity);
		memcpy(_array, rStack._array, _size);
		//for (int i = 0; i < _size; ++i) {
		//	_array[i] = rStack._array[i];
		//}
	}

为了提高程序效率,一般对象传参时,尽量使用引用类型;函数返回值根据实际场景,能用引用尽量使用引用。

相关推荐
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程1 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye2 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
霁月风3 小时前
设计模式——适配器模式
c++·适配器模式