前言
一、类的引入
在C语言中,结构体中只能定义变量,但是到了C++里,结构体默认已经被升级成"类"了,里面不仅可以定义变量,还可以定义函数;
"类"的关键字是class
虽然struct在C++中也可以用来定义类,一般在C++里还是习惯用关键字class来定义类,格式如下:
class name//自定义类名
{
//成员函数和成员变量
};
类中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法/成员函数。
关于struct和class的区别,目前可以先这样李姐:
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来
定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类
默认访问权限是private。
加入我想写一个栈的类,函数的部分也有两种写法供我选择:
第一种写法是把函数的声明和定义全放在类里面,eg:
class Stack
{
public:
//初始化栈
void StackInit(int n)
{
a = (int*)malloc(sizeof(int) * 4);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = n;
}
//等等其他成员函数
private:
int* a;
int top;//栈顶
int capacity;//容量
};
但是不建议像上面这样写,一般把声明放在类里,定义放在头文件中
头文件Stack.h内容:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
using namespace std;
class Stack
{
public:
//初始化栈
void Init(int n);
void Push(int x);
//等等其他成员函数
private:
int* a;
int top;//栈顶
int capacity;//容量
};
Stack.cpp内容:
```cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
void Stack::Init(int n)
{
a = (int*)malloc(sizeof(int) * 4);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = n;
}
void Stack::Push(int x)
{
assert(a);
if (top == capacity)
{
int* tmp = (int*)realloc(a, 2 * capacity * sizeof(int));
if (tmp == NULL)
{
perror("StackPush:realloc\n");
exit(-1);
}
a = tmp;
capacity *= 2;
}
a[top] = x;
top++;
}
使用Stack.h的文件中的内容:
>注意在头文件的生命中要额外加上"类名::",换句话说,类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符来指明成员属于哪个类域。
```cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
int main()
{
Stack st;
st.Init(4);
st.Push(1);
st.Push(2);
return 0;
}
二、类的访问
1.访问限定符
访问限定符就是前文代码中的public、private,以及暂且未提到的protect
public在类外面可以随便访问,其余两类只有在类里面才能随便访问,类外面访问的时候需要加访问限定符
注意访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
;如果后面没有访问限定符,作用域就到 }即类结束。
2、类的大小/存储方式
虽然函数的定义既可以放在类的内部,也可以放在头文件里,但实际上类的大小的计算规则仍然遵循结构体大小的计算规则,但计算过程中一定要注意内存对齐,不熟悉结构体内存对齐规则的话可以看一下我的这一篇博客:点击进入传送门
然后看一下下面这段代码,猜一下运行结果吧:
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
class A1
{
int a;
};
class A2
{
void fuc()
{}
};
class A3
{
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
return 0;
}
答案:
结论是注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
这一点其实也暗示了类的存储方式:
就算是我们把函数的声明和定义全都放进类里面,成员函数实际存储的位置也不在类里面,而是在公用的代码块中
三、this指针
1、意义
当我们声明了多个类型相同的结构体时,编译器会自己把我们写的函数做一些小处理,通过加入一个隐含的形参,区分它正在对哪个结构体进行操作, 拿上文中提到过的push函数来说,经过编译器的处理之后,函数实际上是这个样子:
void Stack::Push(Stack* this,int x)
{
assert(this->a);
if (top == capacity)
{
int* tmp = (int*)realloc(a, 2 * capacity * sizeof(int));
if (tmp == NULL)
{
perror("StackPush:realloc\n");
exit(-1);
}
this->a = tmp;
this->capacity *= 2;
}
this->a[this->top] = x;
this->top++;
}
:C++编译器给每个“非静态的成员函数“增加了一个隐藏
的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”
的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编
译器自动完成。
vs下编译器把this指针存储在ecx寄存器中
2、细节
我们刚刚在"类的存储方式"这部分中提到过,类的成员函数是存储在公用代码段里面的,根据这个提示还可以弄出一些题目,比如说猜一猜下面这个程序,是编译报错,运行崩溃,还是正常运行呢?
```cpp
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
/
/
答案是正常运行
我觉得如果回答错误的话,多半是被这个箭头"->"给误导了,在C语言中,当然可以认为这是在对空指针进行解引用访问里面的成员,但实际上成员函数Print不在对象p里面,地址要在公共区域(代码段)里去找,有箭头但是没有解引用,实际意义是p传递给了this指针,Print虽然把nullptr作为this指针运行,但函数内部没有对nullptr动手动脚,因此程序正常运行
接下来把代码小小地改动一下:
class A
{
public:
void Print()
{
cout << _a << endl;//改为输出成员
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
虽然只改了一行,但是这样程序会崩:
因为在修改过的Print函数中对结构体成员进行了访问,也就是对空指针进行了解引用操作
四、类的默认成员函数
类一共有6个默认成员函数,这篇博客暂且只讲3个,其余部分会放在下期博客
1、构造函数
概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
沿用之前的例子吗,每当你声明一个新的Stack类的变量时,构造函数就会在这个新变量实例化的时候调用一次
之前举例用的Init改一下就是一个能用的构造函数了:
构造函数可以重载
Stack.h:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
using namespace std;
class Stack
{
public:
//初始化栈
Stack();//构造函数
Stack(int n);//构造函数
void Push(int x);
~Stack();//析构函数
//等等其他成员函数
private:
int* a;
int top;//栈顶
int capacity;//容量
};
Stack.cpp:
Stack::Stack()
{
a = (int*)malloc(sizeof(int) * 4);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = 4;
}
//Stack::Stack(int n=4)
//{
//
// a = (int*)malloc(sizeof(int) * n);
// if (a == nullptr)
// {
// perror("malloc fail");
// exit(-1);
// }
//
// top = 0;
// capacity = n;
//}
Stack::Stack(int n)
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = n;
}
上述代码里注释中的内容不能代替有参时对应的构造函数,否则在无参时两个函数都适用,编译器会因困惑而产生报错;
以下两种写法都会默认调用构造函数:
Stack eg1;//写法固定,不能用Stack eg1();
Stack eg2(4);
如果我们不向上面一样显式定义构造函数,由于C++规定对象在实例化时必须调用构造函数,于是编译器会自己生成一个构造函数
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型
构造函数特点如下:
对内置类型的变量不做处理,对自定义类型成员调用默认构造函数(不用传参数的构造)
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
但是编译器自己生成的默认构造函数比较垃圾,打印出来的话经常会看到一堆随机值
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
2、析构函数
特点:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数;析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
在vs下按f11逐步调试的话,会发现在return 0之后,调试过程会走到析构函数里:
Stack::~Stack()
{
if (a)
{
free(a);
a = NULL;
capacity = 0;
top = 0;
}
}
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
using namespace std;
class Stack
{
public:
//初始化栈
Stack();//构造函数
Stack(int n);//构造函数
void Push(int x);
~Stack();//析构函数
//等等其他成员函数
private:
int* a;
int top;//栈顶
int capacity;//容量
};
其他细节和构造函数相似:如果我们不显式定义析构函数,编译器会自动生成一个析构函数,它也是对内置类型的成员不处理,自定义类型的成员调用析构函数
如果你在程序中动态申请了一块空间,一定记得在自己写的析构函数里把相应的空间释放
3、拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象对内置类型按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝;对于自定义类型,则会调用默认的拷贝构造函数
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数一定要写
像是Stack里面的a,直接赋值会导致两个指针指向同一块空间,正确写法:
Stack::Stack(const Stack& n)
{
a = (int*)malloc(sizeof(int) * (n.top));
memcpy(a, n.a, sizeof(int) * (n.top));
top=n.top;//不要弄混拷贝对象
capacity = n.capacity;
}
之所以采用传引用的写法,是因为传值写法会导致无限递归;
原因在于传值时,形参为实参的一份临时拷贝,传值调用的这个过程也需要调用拷贝构造函数,接下来调用的拷贝构造函数又接收到传值,于是继续调用拷贝构造函数…这样一步一步调用下去没完没了
示例代码:
调用拷贝构造函数的时候也可以像注释里那样写
运行结果:
可知拷贝成功