本系列暴死,不会更新了。

该系列为本人的学习笔记,主要由本人整理书写而成。部分内容来自教材、视频课程等,不能保证完全原创性。

萌新的学习笔记,写错了恳请斧正。

# C++ 中的结构体

我们在 C 语音中学的结构体在 C++ 依旧适用,而且增加了其他的功能。

在 C++ 中,结构体不仅可以用来定义变量,还可以定义函数

在这个函数内,还可以直接使用结构体中的变量。

比如之前我们在数据结构中实现的栈现在可以这样全部放在结构体内部:

#include <iostream>
using namespace std;
typedef int DataType;
struct Stack
{
	void Init(size_t capacity)
	{
		m_arr = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == m_arr)
		{
			perror("malloc");
			return;
		}
		m_capacity = capacity;
		m_size = 0;
	}
	void Push(const DataType& data)
	{
		m_arr[m_size] = data;
		++m_size;
	}
	void DataType Top()
	{
		return m_arr[m_size - 1];
	}
	void Destory()
	{
		if (nullptr != m_arr)
		{
			free(m_arr);
			m_arr = nullptr;
			m_capacity = 0;
			m_size = 0;
		}
	}
	DataType* m_arr;
	size_t m_capacity;
	size_t m_size;
};
int main()
{
	Stack stack;
	stack.Init(10);
	stack.Push(1);
	stack.Push(2);
	stack.Push(3);
	stack.Push(4);
	
	cout << stack.Top() << endl;
	stack.Destory();
	return 0;
}

我们发现,这样把一类方法的所有函数和变量封装在一起的方式非常高效,便于代码调用和管理。

在 C++ 中,引入了一个更加适合这样的封装调用的方式,那就是类(Class)

# 类的定义

class classname
{
    // 类体,包括成员变量和成员函数
};	// 注意这里要加分号,与结构体类似

class 是 C++ 的关键字,用于定义类;classname 是类的名字;花括号中是类的主体。

同样的,我们可以在类体中放成员变量和成员函数,但是类中成员函数的定义可以有两种方式:

  1. 定义与声明都放在类中,对于一些简单的小函数直接定义在类中比较方便:

    class Date
    {
        void SetDate(int y, int m, int d)
        {
            m_year = y;
            m_month = m;
            m_day = d;
        }
        
        int m_year;
        int m_month;
        int m_day;
    }

    在这种情况下,编译器更倾向于将这样的函数作为内联函数处理。

  2. 定义放在类外,声明放在类中,这样可以让拥有较多较大函数的类看起来更直观:

    #include <iostream>
    using namespace std;
    typedef int DataType;
    class Stack
    {
    	void Init(size_t capacity);
    	void Push(const DataType& data);
    	DataType Top();
    	void Destory();
    	DataType* m_arr;
    	size_t m_capacity;
    	size_t m_size;
    };
    void Stack::Init(size_t capacity)
    {
        m_arr = (DataType*)malloc(sizeof(DataType) * capacity);
        if (nullptr == m_arr)
        {
            perror("malloc");
            return;
        }
        m_capacity = capacity;
        m_size = 0;
    }
    void Stack::Push(const DataType& data)
    {
        m_arr[m_size] = data;
        ++m_size;
    }
    DataType Stack::Top()
    {
        return m_arr[m_size - 1];
    }
    void Stack::Destory()
    {
        if (nullptr != m_arr)
        {
            free(m_arr);
            m_arr = nullptr;
            m_capacity = 0;
            m_size = 0;
        }
    }

    我们发现,这样声明与定义分离可以让类的结构变的简单明了。但是在类外再对这些函数作定义时就需要在函数名前加上类名和作用域限定符,就像上一篇中的命名空间域一样。

    其实,这也是一个域,叫类域。

# 类域

类域与局部域、全局域、命名空间域类似,一个类中的成员变量和成员函数都属于这个类域。

# 访问限定符

在 C++ 中,类与结构体的成员是有访问权限这么一个属性的。访问权限有 public(公有)private(私有)、**protected(保护)** 三种。我们可以通过访问限定符来设定成员的访问权限:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	void Init(size_t capacity);
	void Push(const DataType& data);
	DataType Top();
	void Destory();
private:
	DataType* m_arr;
	size_t m_capacity;
	size_t m_size;
};
void Stack::Init(size_t capacity)
{
    m_arr = (DataType*)malloc(sizeof(DataType) * capacity);
    if (nullptr == m_arr)
    {
        perror("malloc");
        return;
    }
    m_capacity = capacity;
    m_size = 0;
}
void Stack::Push(const DataType& data)
{
    m_arr[m_size] = data;
    ++m_size;
}
DataType Stack::Top()
{
    return m_arr[m_size - 1];
}
void Stack::Destory()
{
    if (nullptr != m_arr)
    {
        free(m_arr);
        m_arr = nullptr;
        m_capacity = 0;
        m_size = 0;
    }
}

我们看到,在上面的程序中,从 public: 之后,一直到下一个访问限定符之前的内容都会被设置为公有访问权限,而 private 和 protected 同理。

那么不同的访问权限到底意味着什么呢?

  1. public 修饰的成员在类外可以直接被访问。
  2. protected 和 private 修饰的成员在类外不能直接被访问(此处 protected 和 private 是类似的)。
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
  4. 如果后面没有访问限定符,作用域就到子即类结束。
  5. 如果没有加访问限定符,class 的默认访问权限为 private,struct 为 public。(因为 struct 要兼容 C)

# 类的实例化

用类类型创建对象的过程,称为类的实例化:

  1. 类是对对象进行描述的,是一个模型或者图纸一样的东西,限定了类有哪些成员。与结构体相同,定义出一个类并没有分配实际的内存空间来存储它

  2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类的成员变量(不包括成员函数,原因下面说)

    class Date
    {
    public:
        void SetDate(int y, int m, int d)
        {
            m_year = y;
            m_month = m;
            m_day = d;
        }
    private:
        int m_year;
        int m_month;
        int m_day;
    };
    int main()
    {
        Date date;	// 实例化,对象 date 就是类 Date 实例化的结果
        date.SetDate(2024, 5, 11);
        
        return 0;
    }

# 类对象模型

我们如何计算类对象的大小?这就需要我们知道类对象是如何存储的。

其实很简单,我们用 sizeof 试试就知道,类对象的大小只包括类的成员变量的大小(与结构体一样遵循内存对齐规则,详见 C 语言笔记 #28

那么成员函数呢?

成员函数放在公共代码区的一个类成员函数表中。

# this 指针

我们还是以日期类为例:

class Date
{
public:
    void SetDate(int y, int m, int d)
    {
        m_year = y;
        m_month = m;
        m_day = d;
    }
private:
    int m_year;
    int m_month;
    int m_day;
};
int main()
{
    Date date1;
    date1.SetDate(2024, 5, 11);
    
    Date date2;
    date2.SetDate(2024, 5, 12);
    
    return 0;
}

我们看到,对于 Date 类的两个对象,我们都调用了函数 SetDate。

但是这个函数只有三个参数 y、m 和 d,而且上面提到类成员函数是储存在公共代码区的,所以两次调用的是同一个地址。

那么 SetDate 函数是如何知到调用时应该对那一个对象进行操作的呢?为什么 date1.SetDate(2024, 5, 11); 不会去修改对象 date2 的成员变量呢?

在 C++ 中,是通过引入 this 指针来解决这个问题的:

C++ 编译器给每个 “非静态的成员函数” 增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有 “成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

所以上面函数定义中也可以这样写:

void SetDate(int y, int m, int d)
{
    this->m_year = y;
    this->m_month = m;
    this->m_day = d;
}

# this 指针的特性

  1. this 指针的类型: 类的类型* const ,const 意味着在成员函数中不能给 this 指针赋值
  2. 只能在成员函数内部使用
  3. 本质上是成员函数的形参,当对象调用成员函数时将对象的地址作为实参传递给 this 实参。所以对象中不存储 this 指针
  4. this 指针是成员函数的第一个隐含的指针形参,一般由 ecx 寄存器自动传递,不需要用户传递

# 对比 C 与 C++Stack 的实现

# C 语言

Stack.h
#pragma once
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
// 数据类型
typedef int STDataType;
typedef struct Stack
{
	STDataType* _data;
	int _top;
	int _capacity;
} Stack, * pStack;
// 栈的初始化
void InitStack(pStack pst);
// 压栈
void StackPush(pStack pst, STDataType x);
// 弹栈
void StackPop(pStack pst);
// 栈顶元素
STDataType StackTop(pStack pst);
// 栈大小
int StackSize(pStack pst);
// 栈判空
_Bool StackEmpty(pStack pst);
// 栈销毁
void StackDestory(pStack pst);
Stack.c
#include "Stack.h"
// 栈的初始化
void InitStack(pStack pst)
{
	assert(pst);
	pst->_data = NULL;
	pst->_top = 0;
	pst->_capacity = 0;
}
// 容量检查扩容
void StackCheck(pStack pst)
{
	if (pst->_top == pst->_capacity)
	{
		int newCapacity = ((pst->_capacity == 0) ? 4 : (2 * pst->_capacity));
		STDataType* temp = (STDataType*)realloc(pst->_data, newCapacity * sizeof(STDataType));
		if (temp == NULL)
		{
			perror("realloc");
			exit(EXIT_FAILURE);
		}
		pst->_data = temp;
		pst->_capacity = newCapacity;
	}
}
// 压栈
void StackPush(pStack pst, STDataType x)
{
	assert(pst);
	StackCheck(pst); 
	memcpy(&(pst->_data[pst->_top]), &x, sizeof(STDataType));
	pst->_top++;
}
// 弹栈
void StackPop(pStack pst)
{
	assert(pst);
	assert(!StackEmpty(pst));
	pst->_top--;
}
// 栈顶元素
STDataType StackTop(pStack pst)
{
	assert(pst);
	return pst->_data[pst->_top - 1];
}
// 栈大小
int StackSize(pStack pst)
{
	assert(pst);
	return pst->_top;
}
// 栈判空
_Bool StackEmpty(pStack pst)
{
	assert(pst);
	return pst->_top == 0;
}
// 栈销毁
void StackDestory(pStack pst)
{
	assert(pst);
	free(pst->_data);
	pst->_capacity = 0;
	pst->_top = 0;
	pst->_data = NULL;
}

# C++

先忽略 Stack 函数与~Stack 函数,下一篇笔记讲。

Stack.h
#pragma once
#include <iostream>
#include <string>
using namespace std;
typedef int StackDataType;
class Stack
{
public:
	Stack(int size);
	~Stack();
	void Push(StackDataType data);
	StackDataType Pop();
	StackDataType Top();
	bool IsEmpty();
	bool IsFull();
	int GetSize();
	int GetTop();
private:
	StackDataType* m_data;
	int m_size;
	int m_capacity;
};
Stack.cpp
#include "Stack.h"
Stack::Stack(int size = 4)
{
	m_data = new StackDataType[size];
	m_size = 0;
	m_capacity = size;
}
Stack::~Stack()
{
	delete[] m_data;
}
void Stack::Push(StackDataType data)
{
	if (IsFull())
	{
		cout << "Stack is full!" << endl;
		return;
	}
	m_data[m_size++] = data;
}
StackDataType Stack::Pop()
{
	if (IsEmpty())
	{
		cout << "Stack is empty!" << endl;
		return -1;
	}
	return m_data[--m_size];
}
StackDataType Stack::Top()
{
	if (IsEmpty())
	{
		cout << "Stack is empty!" << endl;
		return -1;
	}
	return m_data[m_size - 1];
}
bool Stack::IsEmpty()
{
	return m_size == 0;
}
bool Stack::IsFull()
{
	return m_size == m_capacity;
}
int Stack::GetSize()
{
	return m_size;
}
int Stack::GetTop()
{
	return m_size - 1;
}