一,类的引入
在C语言中,结构体内只允许定义变量,不允许定义函数。
在C++中,结构体内不仅可以定义变量,而且可以定义函数。
以我们手动创建的顺序表为例子:
以前C语言实现的时候是定义一个结构体存储数据,并提供了一些全局函数来处理数据
C++中,将数据和处理数据的方法(即函数)都放到结构体的内部。
struct SeqList
{
void init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capcity = n;
}
void Destroy()
{
if (_a)
{
free(_a);
_a = nullptr;
_size = _capcity = 0;
}
}
void Push_Back(int x)
{
if (_size == _capcity)
{
// ...
}
_a[_size++] = x;
}
int* _a;
int _size;
int _capcity;
};
int main()
{
SeqList L1;
L1.init();
L1.Push_Back(1);
L1.Push_Back(2);
L1.Push_Back(3);
L1.Destroy();
return 0;
}
与C语言实现相比,C++简化了一些。
C语言定义的全局函数一般都要著名是关于顺序表的函数:SeqListInit。
C语言中在调用相关函数的时候,都要传一个顺序表结构体的指针变量。
二,类的定义
C++中一般不用struct关键字来定义类,而是用class关键字来定义类。
类中的变量叫做成员变量。
类中的函数叫做成员函数。
class classname
{
// 成员函数 ...
// 成员变量 ...
};
成员函数定义的两种方式
- 类内声明并定义
类内声明并定义的函数,需要注意一点的是,编译器可能会将其当作内联函数来处理。 - 类内声明类外定义
一般来说都是将类定义在头文件中,在源文件中定义成员函数,要注意的是由于类确定了一个新的作用域,而成员函数是在类的这个作用域下的,所以在类外定义成员函数的时候要加访问限定符。
class SeqList
{
void init(int n = 4);
void Destroy();
void Push_Back(int x);
int* _a;
int _size;
int _capcity;
};
void SeqList::init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capcity = n;
}
void SeqList::Destroy()
{
if (_a)
{
free(_a);
_a = nullptr;
_size = _capcity = 0;
}
}
void SeqList::Push_Back(int x)
{
if (_size == _capcity)
{
// ...
}
_a[_size++] = x;
}
三,类的访问限定符
C++中类的访问限定符有三种
public
被public修饰的成员允许在类外被直接访问
private
被private修饰的成员不允许在类外直接访问
protected
被protected修饰的成员不允许在类外直接访问
注意
- 访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现为止,若后面不再有访问限定符的出现,那么作用域就到类的结束。
- class默认的访问权限是private,struct默认的访问权限是public。
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
四,类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域。
class SeqList
{
public:
void init(int n = 4);
void Destroy();
void Push_Back(int x);
private:
int* _a;
int _size;
int _capcity;
};
void SeqList::init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capcity = n;
}
void SeqList::Destroy()
{
if (_a)
{
free(_a);
_a = nullptr;
_size = _capcity = 0;
}
}
void SeqList::Push_Back(int x)
{
if (_size == _capcity)
{
// ...
}
_a[_size++] = x;
}
五,类的实例化
- 类的声明是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
- 一个类可以实例化出多个对象,实例化出来的对象是占用物理空间的。
- 类的声明好比是建房子的图纸,而实例化出来的对象是一栋栋的房子。
六,类的对象大小的计算
- 首先说明,类中的成员变量是存储在类上的,而成员函数是不存储在类上的,而是存在代码段上的。(因为即使是不同的对象他们的成员变量的数据可能不同,但是他们调用的成员函数都是相同的,如果成员函数在每个对象中都存储一份,那么太浪费空间了)
- 类的成员变量的对齐方式与C语言中struct结构体的对齐方式相同
结构体对齐方式
1,1. 第一个成员在相对结构体变量地址偏移量为0的位置。
2,其他成员变量要对齐到对齐数的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3,结构体的总大小:为最大对齐数(所有成员对齐数中最大的)的整数倍。
4,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
注意 - 一个空类实例化出来的对象的大小是一个字节,是编译器用来标识这个对象的。
七,类成员函数的this指针
你有没有发现一个问题:
一个类实例化出了多个对象,当这多个对象都调用同一个成员函数的时候,明明没有传对象的地址,成员函数是如何区分具体是哪个对象在调用它呢?
那是因为有this指针的存在。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
- this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针,this指针是存在与成员函数的栈帧上的。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递
- 两个经典问题
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
很多初学者会认为,这个程序妥妥的会崩溃,由于发生了空指针的解引用。
但是,答案却不是这样的。
由于Print这个成员函数,并没有存储在某个实例化出的对象上,所以p指针想要调用这个函数并不需要对这个对象的指针解引用得到对象,再去对象中调用函数。
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
这个程序妥妥的会崩溃,由于PrintA函数中访问了_a这个成员变量,虽然成员函数是不存储在对象上的,但是成员变量是存储在对象上的,想要访问_a就要对指针解引用。
总结
即使一个指针变量p指向的是nullptr,但 p->… ,还是*p … 都并不一定造成解引用空指针,而是要看它具体的作用是干什么。