类的定义
类是一种自定义类型,定义它需要用到关键字 class。在类中声明的成员变量又称为类的属性,成员函数称为类的行为。
类定义格式
定义一个类的方式为 class + 类名 + { };
cpp
//Stack类
class Stack
{
void Init()//该类的成员函数,又称为类的行为
{
;
}
//.....
int* array;//类的成员变量,又称为类的属性
int size;
int capacity;
//...
};
这就是一个类的定义格式,一个类就是一个自定义类型,该类型的名字就是类名,这里我们定义的类型就是 Stack,我们在定义该类对象时类型就可以直接写 Stack,不需要前面加上 class。
在传统 C 语言中也有自定义类型结构体 struct。
c
struct C
{
int a;
};
struct C b;
但是在定义该类型对象时,类型名必须是 struct C,比较麻烦,所以在 C++ 中对 struct 进行了升级,后面的结构体名就是类型名,允许 struct 定义类,内容可以包括成员函数和变量。
cpp
struct CPP
{
void Func();
int a;
};
CPP c;
通常情况下我们还是用 class 来定义类。
定义在类里面的函数会被默认成inline函数,定义在类外面的不会。
cpp
//Stack类
class Stack
{
void Init(int n = 4)//定义在类里面,默认成inline函数
{
array=(int*)malloc(sizeof(int)*n);
if (array == NULL)
{
perror("malloc");
exit(-1);
}
size = 0;
capacity = n;
}
void Push(int x);//定义在类外面
int* array;//类的成员变量,又称为类的属性
int size;
int capacity;
};
void Stack::Push(int x)
{
//判断是否需要扩容
if (size == capacity)
{
int* p = (int*)realloc(array, sizeof(int) * capacity * 2);
if (p == NULL)
{
perror("realloc");
exit(-1);
}
array = p;
p = NULL;
capacity *= 2;
}
//入栈
array[size++] = x;
}
这里成员函数Push定义在类外面,但是函数名位于类,也就是类域里面,编译器不会主动去类域里找,同样也需要作用域解析符 :: 让编译器去该类里面找函数名。
访问限定符
C++ 提供一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用。
| 访问限定符 | 作用 |
|---|---|
| public | 允许成员在类外被直接访问。 |
| private | 不允许成员在类外被直接访问。 |
| protected | 在当前可认为与 private 相同的作用,不同点会在后续继承的学习中体现。 |
访问限定符的作用范围从开始修饰成员到下一个访问限定符出现为止,如果没有遇到访问限定符,就作用到 }; 为止。
cpp
//Stack类
class Stack
{
//公有成员,在类外可被访问
public:
void Init(int n = 4)//定义在类里面,默认成inline函数
{
array=(int*)malloc(sizeof(int)*n);
if (array == NULL)
{
perror("malloc");
exit(-1);
}
size = 0;
capacity = n;
}
void Push(int x);//定义在类外面
//public作用范围到private之前
private:
int* array;//类的成员变量,又称为类的属性
int size;
int capacity;
//没有遇到下一个访问限定符,遇到了 };
};//private作用范围到此结束
当我们没有使用访问限定符修饰成员时,class的成员默认为private,struct则默认为public。
一般类的属性会被定义成private/protected,行为是public。
类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
我们上面 Push 的定义也指明了在类外定义成员的方法。
cpp
//返回类型 类域名::成员函数名(参数列表)
void Stack::Push(int x)
{
//判断是否需要扩容
if (size == capacity)
{
int* p = (int*)realloc(array, sizeof(int) * capacity * 2);
if (p == NULL)
{
perror("realloc");
exit(-1);
}
array = p;
p = NULL;
capacity *= 2;
}
//入栈
array[size++] = x;
}
在 C++ 中域有四域,局部域、全局域、命名空间域,然后还有类域,其中命名空间域和类域不影响变量的生命周期,影响编译规则。它们的存在主要是为了解决命名冲突的问题,类域的存在解决类和类之间的命名冲突问题。
编译器只会去局部域和全局域里面查找对象,不会去命名空间域和类域里面查找,所以需要我们用 :: 指定访问空间。
实例化对象
概念
用类类型在物理内存中创建对象的过程,称为类实例化出对象。
cpp
//Stack类
class Stack
{
public:
void Init(int n = 4)
{
array=(int*)malloc(sizeof(int)*n);
if (array == NULL)
{
perror("malloc");
exit(-1);
}
size = 0;
capacity = n;
}
void Push(int x);
private:
int* array;
int size;
int capacity;
};
int main()
{
Stack.Init;
return 0;
}
这里的类只是一份声明,编译器并没有为其分配内存空间,所以直接使用会报错。

我们需要创建类类型的对象,也可以说用类实例化出一个对象,编译器才会为该对象分配内存空间,我们就可以使用该对象了。
cpp
Stack st;//用Stack类实例化出一个对象st
//可以使用该对象
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
类的大小
一个类里面包含成员函数和成员变量,成员变量的大小好说,那成员函数呢?
cpp
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
我们知道结构体的大小遵循内存对齐规则,类也是如此。
内存对齐规则
- 首元素从起始位置开始放置
- VS 默认对齐数为 8
- 元素类型与 VS 默认对齐数相比取最小对齐数,将该元素对齐到最小对齐数的整数倍处。
- 结构体的大小是 VS 默认对齐数与元素对齐数(所有元素类型中取最大对齐数)中小的对齐数的整数倍
假定我们的成员函数也在类里面放置,以函数指针的形式存放。
遵循内存对齐规则,Date 类中成员变量所占字节数为 12,然后一个成员函数名是 int* 指针,我们是 X64 环境,占 8 个字节,对齐到 8 的整数倍处,存放位置为 16 ~ 23,这样总共占 24 个字节,类大小对齐到 8 的整数倍处,所以类的大小应该是 24。
我们来验证一下:
cpp
Date d;
cout << sizeof(Date) << endl;
cout << sizeof(d) << endl;

结果是 12 个字节,也就是成员函数不存放在类里面,对象里面也不会,编译器把它存到其他地方了。
因为一个类的不同对象调用同一个成员函数,比如说打印功能函数Print(),如果在每个对象中都给这个成员函数分配了内存空间的话,就会出现内存浪费的问题。
那么下列程序呢?
cpp
class a
{
void Func();
};
class b
{
};
int main()
{
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
return 0;
}
成员函数空间不存类里面,类a的大小是0吗?类b中啥也不存,大小也是0吗?是不是感觉很不对劲。

结果都是1,因为类大小为0,就说不通有这个类存在了,所以会分配一个字节的占位符用来标记这个类存在。