一、类的引入
在C语言中,struct 只能定义变量
比如,在C语言中定义一个栈,我们最关注的是这些函数的过程的实现。
struct Stack
{
int* a;
int top;
int capacity;
};
void Stack_Init(struct Stack* ps);
void Stack_Push(struct Stack* ps, int x);
void Stack_Pop(struct Stack* ps);
//......
而C++中不仅兼容 C struct 的用法,同时将 struct 升级成了类,也就是说C++对 struct 进行了升级,它不仅可以定义变量,还能定义函数。
struct Stack
{
void Init()
{
//...
}
void Push(int x)
{
//...
}
void Pop()
{
//...
}
int* a;
int top;
int capacity;
};
然后就可以直接这样调用这个函数
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Pop();
return 0;
}
同时在C++中,类名就是类型,可以直接这样定义不需要 typedef
int main()
{
struct Stack st1; //C
Stack st2; //C++
return 0;
}
但是C++中定义类还是更喜欢用 class
二、类的访问限定符
| 访问限定符: | public(公有) | protected(保护) | private (私有) |
public (公有) 就是在类里面可以访问,类外面也可以访问
private (私有) 就是在类里面可以访问,类外面不能访问
现阶段 protected 和 private 类似
如果把刚才 struct 定义的东西改成 class
-
class Stack { void Init() { //... } void Push(int x) { //... } void Pop() { //... } int* a; int top; int capacity; }; int main() { Stack st; st.Init(); st.Push(1); st.Pop(); return 0; }
然后编译一下,就会发现它报错了,说 无法访问 私有。
是因为我们没有给 class 访问限定符,class 默认访问权限为 private,而 struct 为 public(因为 struct 要兼容C)
如果是想给别人用的就设置成 公有的
class Stack
{
public:
void Init()
{
//...
}
void Push(int x)
{
//...
}
void Pop()
{
//...
}
直到遇到下一个访问限定符
不想给别人用的设置成 私有的
private: int* a; int top; int capacity; };
三、类的两种定义 1,声明和定义全部放在类中
成员函数如果在类中定义,函数符合inline条件(编译指令比较少)会将其当成内联函数处理
2,声明和定义分离假设要实现一个队列,就是头文件里面放结构的定义和成员函数的声明
//.h
class QueueNode
{
public:
int val;
QueueNode* next;
};
class Queue
{
public:
void Init();
void Push(int x);
void Pop();
private:
QueueNode* head;
QueueNode* tail;
};
函数的定义放到源文件里面
//.cpp
void Queue::Init()
{
//...
}
void Queue::Push()
{
//...
}
void Queue::Pop()
{
//...
}
因为类自己也是一个域,所以在类体外定义成员需要用 :: 作用域解析符指明成员属于哪个类域,所以前面要加一个 Queue:: 如果不加的话它默认是个普通函数,只会在局部找,再去全局找,再去展开的命名空间找,都找不到(C++中只要是 { } 括起来的都叫域,域要么影响生命周期,要么影响访问)
声明和定义分离的意义在于方便阅读代码,比如一个类里面光成员函数就有一百个,这个时候把声明和定义放在一起去看这个代码的时候就很不方便。
类的定义:
1,小函数想成为inline直接在类里面定义即可
2,如果是大函数,声明和定义应该分离
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现的细节,仅对外公开接口来和对象进行交互。封装本质上是种管理,让用户更方便使用。
比如:对于电脑这样一个复杂的设备,提供给用户的只有开关机、键盘输入、显示器、USB插孔等,让用户和计算机交互,但实际上电脑真正工作的是它的那些硬件元件。对于使用者而言,不必去关心内部核心部件,线路布局,CPU的设计等。
四、类的实例化在这个类里面的成员都是声明,函数的声明和定义很容易区分,那变量的声明和定义怎么区分就要看它有没有开空间,开空间的就是定义,不开空间的就是声明。
int a; //定义
class Book
{
public:
void PrintInformation(); //声明
public:
char name[20]; //声明
int price;
int number;
};
只有用这个类去定义一个对象的时候它才会开空间,这个过程就叫类的实例化。
类它是不占空间的,它要去实例化出对象,这个对象才占空间。
int main()
{
Book b;
return;
}
那又有一个问题,它在没有开空间的情况下,用 sizeof 去算这个大小,它是去怎么算它的大小的呢?
做个比方,形象一点说就是用图纸去建造房子一样,类呢就是一个设计图,这个设计图不能住人,但是可以根据这个图纸来算这个房子有几个房间,每个房间能住几个人,一合计,也能算出来,不一定要实例化后才能算。
int main()
{
cout << sizeof(Book) << endl; //28
return 0;
}
五、类对象模型
我们如何来算这个类的大小呢?
class A
{
public:
void Print()
{
cout << a << endl;
}
private:
char a;
};
从结果来看,说明 Print 不算成员函数
int main()
{
cout << sizeof(A) << endl; //1
return 0;
}
那为什么它在这里不算成员函数呢?这就和它的存储方式有关,它用的就是下边第三种方式。
类对象的存储方式一种呢就是它全部储存到一起,但这样的设计缺陷在于浪费空间,如果实例化的对象比较多,每个对象都是独立的空间都要存一个成员函数,而这些成员函数都是一样的,相同的代码就会保存多次。
所以有人就提出了另一种方案,把代码专门提取到一个单独的地方,形成一张表,类似指针数组,这样一来就只需要存一份,不需要存多份,就没有那么多浪费,那么我们要找函数就去这张表里找就可以了。
第三种方案,对象里面只存成员变量,成员函数不占对象的空间,也不用一个指针去指向它,它把成员函数直接存在一个公共的代码段,跟普通的函数一样,这时,对象和成员函数没有关联了,那它又是怎么去找的呢?因为它在编译链接时就根据函数名去公共代码区找到了函数的地址。
那么下面这段代码,是崩溃,还是编译出错,还是正常运行呢?
class A
{
public:
void func()
{
cout << "A" << endl;
}
private:
char a;
};
int main()
{
A* ptr = nullptr;
ptr->func();
return 0;
}
结果是正常运行,如果他采用的是第一种或者第二种,那么它都会崩,第一种是去对象里找这个函数的地址然后调用,第二种是去对象找到函数表的地址去找到这个函数,也会崩,因为 ptr 指向的是一个空指针,而第三种正常运行的原因就是 ptr 不会去对象里面找,所以不会去解引用这个空指针, func 没有存在对象里面,在代码区里,编译的时候就已经找到它的地址了。



