C++类和对象(上)

文章目录

  • 前言
  • [1. 类的定义](#1. 类的定义)
    • 1.1类定义格式
    • [1.2 类与结构体的区别](#1.2 类与结构体的区别)
    • [1.3 访问限定符](#1.3 访问限定符)
    • [1.4 类域](#1.4 类域)
  • [2. 类的实例化](#2. 类的实例化)
  • [3. 对象大小](#3. 对象大小)
  • [4. 结构体内存对齐规则](#4. 结构体内存对齐规则)
  • [5. this指针](#5. this指针)
    • [5.1 用途](#5.1 用途)
    • [5.2 特性](#5.2 特性)
  • END

前言

通过对上一篇(C++入门基础)的学习,我们对C++有了一定的了解,那么现在就来学习C++的一个重要的特性:面向对象

在面向对象编程(Object-Oriented Programming,简称OOP)中,对象通过类(Class)来定义。类是一种抽象的数据类型,它描述了具有相同属性和方法的对象的集合。一个类可以看作是一个模板,用于创建具有特定属性和行为的对象实例。


1. 类的定义

1.1类定义格式

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

cpp 复制代码
class ClassName //类名
{
	//类的主体

}; //以分号结束

当有一个对象的普通实例时,使用 "." 来访问对象的成员。

当有一个指向对象的指针时,使用 "->" 来访问对象的成员。

下面用类来定义和实现一个栈

cpp 复制代码
#include<iostream>
#include<assert.h>
using namespace std;

class Stack //类名
{
	//类的主体
public:	
	//成员函数
	void Init(int n = 4)
	{
		array = (int*)malloc(sizeof(int) * n);
	    if (array == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		capacity = n;
		top = 0;
	}
	void Push(int x)
	{
		// ...扩容
		array[top++] = x;
	}
	int Top()
	{
		assert(top > 0);
		return array[top - 1];
	}
	void Destroy()
	{
		free(array);
		array = nullptr;
		top = capacity = 0;
	}
private:
	//成员变量
	int* array;
	int capacity;
	int top;

}; //以分号结束

int main()
{
	Stack st;//类名变类型
	st.Init();

	st.Push(1);
	st.Push(2);

	cout << st.Top() << endl;

	st.Destroy();
	
	Stack* st1;
    st1 = (Stack*)malloc(sizeof(Stack));
    st1->Init();

    st1->Push(1);
    st1->Push(2);

    cout << st1->Top() << endl;
    st1->Destroy();
	return 0;
}

为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_ 或者 m开头,注意C++中这个并不是强制的,只是一些惯例,

cpp 复制代码
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	// 为了区分成员变量,⼀般习惯上成员变量
	// 会加⼀个特殊标识,如_ 或者 m开头
	int _year; // _year  m_year
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Init(2024, 3, 31);
	return 0;
}

定义在类里面的成员函数默认为inline。

cpp 复制代码
#include<iostream>
#include<assert.h>
using namespace std;
class Date {
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Init(2024, 10, 10);
	return 0;
}

在汇编中没有发现call语句,所以成员函数void Init(int year, int month, int day)是inline函数

在上述例子中,我们都是将成员函数的声明和定义全部放在类里面来实现。我们还可以将成员函数的声明和定义分离:

声明放在Stack.h头文件

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
	void Init(int n = 4);
	void Push(int x);
	int Top();
	void Destroy();
private:
	int* _a;
	int _top;
	int _capacity;
};

定义放在Stack.cpp源文件

cpp 复制代码
#include "Stack.h"

void Stack::Init(int n)
{
	_a = (int*)malloc(sizeof(int) * n);
	if (_a == nullptr)
	{
		perror("malloc申请空间失败");
		return;
	}
	_capacity = n;
	_top = 0;
}
void Stack::Push(int x)
{
	if (_top == _capacity)
	{
		int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
		if (tmp == nullptr)
		{
			perror("realloc申请空间失败");
			return;
		}
		_a = tmp;
		_capacity = newcapacity;
	}
	_a[_top] = x;
	_top++;
}
int Stack::Top()
{
	assert(_top > 0);
	return _a[_top - 1];
}
void Stack::Destroy()
{
	free(_a);
	_capacity = _top = 0;
	_a = nullptr;
}
int main()
{
	Stack st;
	st.Init();
	st.Push(1);
	st.Push(2);
	st.Push(3);
	st.Push(4);

	cout << st.Top() << endl;
	st.Destroy();

	return 0;
}

注意:

在头文件声明了一个类和它的成员(成员函数和成员变量),就创建了类作用域,想在其它文件(如源文件)定义成员函数,就需要用 类作用域限定符:: 来访问成员函数

1.2 类与结构体的区别

C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类。

(1) C语言实现栈

c 复制代码
struct Stack
{
	//成员变量
	int* array;
	int top;
	int capacity;
};
//成员函数
void Init()
{
	// ...
}
void Push(int x)
{
	// ...
}
int Top()
{
	// ...
}
void Destroy()
{
	// ...
}

(2) C++实现栈

cpp 复制代码
struct Stack
{
	//成员变量
	int* _a;
	int _top;
	int _capacity;

	//成员函数
	void Init()
	{
		// ...
	}
	void Push(int x)
	{
		// ...
	}
	int Top()
	{
		// ...
	}
	void Destroy()
	{
		// ...
	}
};

区别:

C语言的struct

1.结构体struct内只能定义变量,不能定义函数,需要在外部定义函数

2.结构体struct内的所有变量默认都是公有的(public)
C++的struct

1.结构体struct升级为类,既可以定义变量,也可以定义函数

2.在 struct 中,默认情况下所有成员都是公有的(public);而在 class 中,默认情况下所有成员都是私有的(private)。

C语言的struct和C++的struct也有所不同:

在C语言中定义结构体变量需要加上关键字struct

c 复制代码
struct Date
{
	int year;
	int month;
	int day;
};
struct Date date;

可以使用typedef将struct Date替换成Date

c 复制代码
typedef struct Date
{
	int year;
	int month;
	int day;
}Date;
Date date;

C语言定义链表结点结构

c 复制代码
typedef int SLTDataType;

typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

在C++中定义结构体变量时,可以忽略关键字struct,可以直接使用结构体名作为类型名

cpp 复制代码
struct Date
{
	int year;
	int month;
	int day;
};
Date date;

C++定义链表结点结构

cpp 复制代码
typedef int SLTDataType;

struct SListNode {
	SLTDataType data;
	SListNode* next;
};

在 C++ 中,编译器默认将 struct、class、union 和 enum 的类型名视为在其作用域内可见,不需要显式的前缀。

C++的class和struct有什么区别?

(1) 默认访问权限不同

  • struct中的成员默认为公有的(public),在类的外部可以访问
  • class中的成员默认为私有的(private),在类的外部不能访问,除非声明为公有的(public)

(2) 继承时的默认访问权限不同

  • 当一个struct继承另一个struct或者一个class时,默认是public继承。
  • 当一个class继承另一个class时,默认是private继承。

(3) 使用场景略有不同

  • struct:通常用于表示简单的数据结构,其目的主要是将相关的数据组合在一起,并且对封装性要求不高的情况。
  • class:更常用于表示具有复杂行为和较高封装性要求的对象。类可以包含成员函数、私有成员变量以及各种访问控制和封装机制,适用于面向对象编程中的复杂对象建模。

1.3 访问限定符

概念:在C++中,访问限定符(Access Specifiers)用于控制类成员的访问权限。它们定义了类的成员(成员变量和成员函数)是否可以在类的外部被访问。

C++有三种访问限定符:public(公有的)private(私有的)protected(受保护的)

(1) public(公有的)

1.被声明为public的成员可以在类的内部和外部被访问到。

2.这意味着可以在任何地方访问该成员,包括类的外部和派生类(如果适用)。

3.通常,将类的接口函数(如获取和设置数据的成员函数)声明为 public,以便用户能够与对象进行交互。

(2) private(私有的)

1.private 成员只能被类的成员函数和友元函数访问。在类的外部无法直接访问私有成员,这有助于实现信息隐藏和封装。

2.这是封装的核心概念,意味着类可以隐藏它的内部状态和实现细节。

3.通过将数据成员声明为 private,并提供 public 的成员函数来访问和修改这些数据,可以更好地控制对数据的访问,提高代码的安全性和可维护性。

(3) protected(受保护的)

1.protected 成员的访问权限介于 public 和 private 之间。

2.protected 成员可以被继承该类的派生类(的成员函数)访问,这在实现类的继承时非常有用。它允许派生类访问基类中的特定成员,同时防止在类的外部直接访问这些成员。

3.这对于在继承中隐藏实现细节但仍允许子类访问这些细节很有用。

注意

  • public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问
  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
  • class类的成员没有被访问限定符修饰时默认为private,struct默认为public。
  • 通过使用访问限定符,可以明确规定哪些成员是对外公开的(public),哪些是内部实现细节(private或protected),从而提高了程序的模块化和封装性。

1.4 类域

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

类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。

cpp 复制代码
#include<iostream>
using namespace std;
class Stack
{
public:
	// 成员函数
	void Init(int n = 4);
private:
	// 成员变量
	int* array;
	int capacity;
	int top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
	array = (int*)malloc(sizeof(int) * n);
	if (nullptr == array)
	{
		perror("malloc申请空间失败");
		return;
	}
	capacity = n;
	top = 0;
}
int main()
{
	Stack st;
	st.Init();
	return 0;
}

2. 类的实例化

概念:用类的类型在物理内存中创建对象的过程,称为类实例化出对象。类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。

具体含义:

类是一种抽象的模板,它定义了对象的共同特征和行为。例如,定义一个 "汽车" 类,这个类可以包含汽车的属性如颜色、品牌、速度等,以及汽车的行为如加速、刹车等。而类的实例化就是根据这个 "汽车" 类创建出一辆具体的汽车对象,每一辆具体的汽车就是 "汽车" 类的一个实例。这个实例拥有类中定义的属性和行为,并且可以通过实例来访问和操作这些属性和行为。比如一辆红色的大众汽车,它具有特定的颜色、品牌等属性,也可以进行加速、刹车等行为。

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//这里的成员变量只是定义,没有开空间
	int _year;
	int _month;
	int _day;
};
int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;
	d1.Init(2021, 1, 29);
	d1.Print();
	d2.Init(2024, 10, 12);
	d2.Print();
	return 0;
}

注意 :不能直接访问类的成员函数,只有用类实例化出对象即分配了内存空间,才能访问对象的成员函数和成员变量(公有的)

3. 对象大小

首先我们用Date类来实例化出一个对象d,那么这个对象的大小(byte)是多少呢?

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	cout << sizeof(d) << endl;
	cout << sizeof(Date) << endl;
	return 0;
}

以上可以看到类Date和对象d的大小为12字节,而_year, _month, _day都是int类型,int类型是4字节,这三个成员变量总共是12个字节。

由此可以推导出 :类对象的大小只计算其成员变量的大小,而不计算成员函数的大小,类对象只存储成员变量,不存储成员函数

那么对象里的成员函数存储在哪里了呢?------>类对象中的成员函数存储在(公共)代码区

代码区:存放程序的机器代码,包括类的成员函数等。当创建类的对象时,成员函数的代码不会在每个对象中重复存储,而是所有对象共享这一份代码。无论创建多少个对象,成员函数的代码都只存在于代码区的一处。

C++内存布局的其它区域:

  • 全局数据区:存储全局变量和静态变量。
  • 栈区:存储局部变量、函数参数等,函数调用时在栈上分配空间,函数执行完毕后自动释放。
  • 堆区:通过动态内存分配(如使用new操作符)获得的内存空间。

如果类对象没有成员变量,只有成员函数,那么这个类对象的大小为多少呢?如果二者都没有呢?

cpp 复制代码
#include<iostream>
using namespace std;

class Date1
{
public:
	void Init()
	{
		
	}
	void Print()
	{
		
	}
};

class Date2
{

};
int main()
{
	Date1 d1;
	cout << sizeof(d1) << endl;

	Date2 d2;
	cout << sizeof(d2) << endl;
	return 0;
}

可以看到,当类对象只有成员函数或者类为空时,类对象的大小为 1 字节。(纯粹是为了占位标识对象存在)

具体原因如下:

当类为空时 :尽管没有任何数据成员和成员函数体占用空间,但 C++ 标准要求每个不同的对象必须有独一无二的地址。为了满足这个要求,即使是空类,编译器也会分配至少 1 个字节的空间,以便在内存中区分不同的对象实例。
当类只有成员函数时:成员函数并不占用类对象的空间,它们的代码存储在代码区。因此,就类对象本身的空间而言,其大小仍然与空类类似,通常为 1 字节。

4. 结构体内存对齐规则

  • 1.第一个成员在与结构体偏移量为0的地址处。
  • 2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 3.VS中默认的对齐数为8
  • 4.对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  • 5.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

内存对齐的原因:

内存对齐主要是为了提高内存访问的效率。现代计算机体系结构中,内存访问通常以特定的字节数为单位进行,如果数据存储的地址不是该单位的整数倍,可能会导致多次内存访问或者降低访问速度。例如,某些处理器对于特定类型的数据(如 4 字节的整数)可能要求其存储地址必须是 4 的倍数,这样可以在一次内存读取操作中高效地获取该数据。

cpp 复制代码
#include<iostream>
using namespace std;

struct Date
{
	char x;
	int y;
};
int main()
{
	Date d;
	cout << sizeof(d) << endl;

	return 0;
}

可以看到类对象d的大小为8字节,但是char为1字节,int为4字节,加起来不是5个字节吗?为什么是8个字节呢?

因为结构体内的成员变量是按照内存规则来存储的

因为char 占 1 字节,int 占 4 字节,所以char 类型的对齐数是1字节,int 类型的对齐数是 4 字节。因此,为了确保 y 在 4 字节对齐的地址上,编译器会在 x 后面添加 3 个字节的填充字节。这样,Date 结构体的大小将会是 8 字节(1 字节的 char + 3 字节的填充 + 4 字节的 int)。

在VS中怎么设置结构体默认的对齐数?

可以使用 #pragma pack 预处理指令来修改结构体的默认对齐数,比如#pragma pack(n),则结构体的默认对齐数为n

5. this指针

通过以上学习知道了类的成员变量存储在类中,成员函数存储在(公共)代码区。那么在类外部调用类对象的成员函数时,成员函数是如何区分是哪个类对象调用它的呢?

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
{
	cout << _year << "/" << _month << "/" << _day << endl;
}
private:
	// 这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};
int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;

	d1.Init(2021, 1, 29);
	d1.Print();

	d2.Init(2024, 10, 12);
	d2.Print();

	return 0;
}

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了一个隐含的this指针解决这里的问题

编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this指针。比如Date类的Init的真实原型为,void Init(Date* const this, int year, int month, int day); 在调用 Init 函数时,编译器会自动传递当前对象的地址,比如:d1.Init(&d1, 2024, 3, 31);

需要注意的是,this作为关键字不需要我们手动写在形参中,编译器会自动传递this指针。同样地,在调用成员函数时也不需要手动传入对象的地址,编译器会自动传递该对象的地址。


this指针通过接收不同对象的地址,就可以根据不同的地址去找到内存中对应的成员变量

5.1 用途

(1) 访问成员变量

cpp 复制代码
void Init(int year, int month, int day)
{
	_year = year;
	//this->_year = year;
	this->_month = month;
	this->_day = day;
}

在Init函数中,第一条语句虽然没有显式地使用this,但实际上编译器会隐式地使用this指针来确定要操作的对象。可以理解为_year = year等价于this->_year = year。

(2) 区分成员函数的参数和成员变量

当成员函数的参数名或局部变量名与成员变量名相同时,可以使用this指针来明确地访问成员变量。例如:

cpp 复制代码
class MyClass
{
public:
	void Init(int num)
	{
		this->num = num;
	}
private:
	int num;
};

这里的this->num明确表示访问对象的成员变量num,而不是函数参数num。

(3) 作为函数参数返回当前对象的引用

可以使用*this作为函数的返回值(返回的是当前对象),允许连续调用成员函数。例如:

cpp 复制代码
#include<iostream>
using namespace std;
class MyClass {
public:
    int data = 0;

    MyClass& addValue(int val) {
        data += val;
        return *this;
    }
};

int main() {
    MyClass obj;
    obj.addValue(5).addValue(10);
    cout << obj.data;
    return 0;
}

在这个例子中,addValue函数返回*this,即当前对象的引用,使得可以连续调用addValue函数。

5.2 特性

  • 1.this指针是指向类对象的指针,例如:MyClass* const this。可以看出this是常量指针,其值(指向对象的地址)不能修改,但是可以通过this指针访问和修改对象的内容(成员变量)
  • 2.与静态成员函数无关 :静态成员函数不属于特定的对象实例,因此在静态成员函数中没有this指针。静态成员函数可以通过类名直接调用,而不需要通过对象来调用。
  • 3.this指针通常存储在函数栈帧(栈区)中:当调用成员函数时会开辟函数栈帧,栈帧为函数分配空间,而this指针是成员函数参数中的一个,自然会给this指针分配空间。但在VS中,this指针会被存储在特定的寄存器(可能是ecx寄存器)中。
  • 4.C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。

接下来看两个例子:

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

可以看到,定义了类A的指针p且p为空,可以正常调用空指针p的成员函数。

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

但是这段函数却报错了,就多了cout << _a << endl;

因为在调用Print函数时,Print函数的this指针接收了传递过来的空指针,而函数体内的_a等价于this->_a,要想访问成员变量_a,就要对this指针进行解引用,但是this指针为空指针,一旦对其解引用就会发生报错。

所以在成员函数内部只要访问成员变量,都会涉及this指针的解引用,只要this指针为空就会报错。

END

对以上内容有异议或者需要补充的,欢迎大家来讨论!

相关推荐
Buleall3 分钟前
期末考学C
java·开发语言
重生之绝世牛码5 分钟前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
小蜗牛慢慢爬行11 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
荒古前17 分钟前
龟兔赛跑 PTA
c语言·算法
Colinnian21 分钟前
Codeforces Round 994 (Div. 2)-D题
算法·动态规划
Algorithm157621 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
用户00993831430126 分钟前
代码随想录算法训练营第十三天 | 二叉树part01
数据结构·算法
shinelord明30 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
დ旧言~36 分钟前
专题八:背包问题
算法·leetcode·动态规划·推荐算法