类
面向对象的三大特性:封装,继承,多态
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:
之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,
会发现struct中也可以定义函数。
C++当中的 struct 升级为了 类,在struct当中除了可以定义变量,还可以定义函数,而且以前C当中的struct 的玩法也可以实现:
struct Stack
{
int* a;
int top;
int capacity;
};//没有进行重命名
int main()
{
struct Stack st1; //C++兼容 C的struct 语法
Stack st2; // 同时 升级为了 类
return 0;
}
我们现在来简单实现一个栈,就可以把栈的当中使用的变量,和函数都写到一个类当中去:
struct Stack
{
void Init(size_t capacity = 4)
{
a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = capacity;
top = 0;
}
void Push(const int& data)
{
// 扩容····书写 这里是省略
a[top] = data;
++top;
}
int Top()
{
return a[top - 1];
}
void Destroy()
{
if (a)
{
free(a);
a = nullptr;
capacity = 0;
top = 0;
}
}
int* a;
int top;
int capacity;
};//没有进行重命名
int main()
{
Stack std;
std.Init();
std.Push(1);
std.Push(2);
std.Push(3);
std.Push(4);
cout << "栈顶元素:" << std.Top() << endl;
std.Destroy();
return 0;
}
我们可以用 ” . “来访问到类当中的函数。
而且上述例子,我们是把成员定义来 函数的下面的,但是我们发现并没有报错,因为C++当中的创建一个类就是创建一个域,而C++是把这个类看做是一个整体的。
在C++当中 跟喜欢 把上述的 struct 的定义方式,用class 来代替,定义一个class和在C++当中定义一个 struct一样,都可以定义成员和函数,在最后都需要 加 分号 “;”。
但是,如果我们直接把上述的 struct 换成 class 就编译不通过了:
在C++当中 给了 三种访问修饰符:
public 在类外可以访问,protected 和 pricate在此处都是一样的,只能再类内使用,后两者在继承当中才有区别。
当我们加了 public修饰符之后,在类外就可以使用了:
class Stack
{
public:
void Init(size_t capacity = 4)
{
a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = capacity;
top = 0;
}
void Push(const int& data)
{
// 扩容····书写 这里是省略
a[top] = data;
++top;
}
int Top()
{
return a[top - 1];
}
void Destroy()
{
if (a)
{
free(a);
a = nullptr;
capacity = 0;
top = 0;
}
}
protected:
int* a;
int top;
int capacity;
};
上述中 三个修饰符之后要交 “ : ” ,而且上述的public 修饰符修饰的范围是,下一个权限修饰符之前,或者是如果没有的话就是一直修饰到最后,如上述,public就修饰到 protected 修饰符的上一行。
上述的访问修饰限定符,不仅仅可以在class 中使用,struct 也可以使用,而我们上述直接把 struct 换成 class 之后就会报错是因为,class,如果不使用 访问修饰限定符的话,他默认是private的。而struct默认是 public修饰的。
但是我们不一般不用默认这个概念,一般都是使用 这个修饰符来修饰。
【访问限定符说明】
- 1. public修饰的成员在类外可以直接被访问
- 2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 4. 如果后面没有访问限定符,作用域就到 } 即类结束。
- 5. class的默认访问权限为private,struct为public(因为struct要兼容C)
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者
成员函数。
类的两种定义方式:
类也是可以声明和定义分离的:
1.)声明和定义全部放在类体中,需注意:成员函数如果在类中定义,默认这个函数是内联的。
如果我们像上述一样在类中去定义 成员函数,这个函数是 内联的,但是同样的,决定一个函数是否是内联的,决定权是在编译器,如果这个函数定义得 复杂,长,那么可能就不会是内敛函数。
2)类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加 “类名::”
上述例子,在头文件中 定义 类的声明,在cpp文件中 定义类中函数的实现,我们用“类名::函数名”这个方式来表示,这个函数是在类中的函数,是成员函数的定义。
需要注意的是:我们一般在写的时候,喜欢把 类当中成员名前加一个 " _ " ,原因是这样的:
class data
{
public:
void Init(int year)
{
year = year;
}
protected:
int year;
int month;
int data;
};
比如这个例子,在Init()函数中的 year = year 是哪个 year 赋值给 year 呢?
其实是这样的赋值的:
因为在成员的函数中的参数访问顺序是,先是 局部域 然后是 类域 最后是 全局域
所以为了防止出现上述的情况,我们就把类当中的 成员加一个" _ " 来区分:
protected:
int _year;
int _month;
int _data;
封装
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来
和对象进行交互。把数据和方法放到一起,不想给用户看的就是 私有的,想给用户看的就是 公有的。
封装本质上是一种管理,让用户更方便使用类,比如一个计算机,有键盘,鼠标,显示屏,开关机键,等等的可以让用户和计算机进行交互的硬件,但是,实际上在完成计算,和工作的是电脑里吗的cpu,显卡,内存等等的这些用户看不到的硬件。厂家在生产电脑的时候就会把这些cpu,显卡,内存等等的给包装起来,把键盘,显示器,开关机键这些给留在外面供用户和计算机交互。
上述就是封装的意义。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来
隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用
不如我们在C中的数据结构,懂得封装的程序员一般是这样写的:
就是,哪怕 STTop()函数只有一行代码,也要写成函数来调用
但是有一些就会 一些用封装,一些就直接访问,如下图:
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域里面,在类体外定义成员的时候,需要使用 :: 作用域操作符指明成员属于那个域的。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
这样也能理解,定义一个类就是一个新的作用域,意思就是我在一个类当中定义了一个 func()函数,我又在另一个类当中定义了一个 func()函数,如果我不指明是那个类当中的func()函数,编译器就不知道我是对哪一个 类 当中的函数进行定义了。
对于函数的声明和定义很好区分,对于变量是声明还是定义,就要判断它开不开空间,如上述类当中的变量就是声明,也它没有开空间。
int main()
{
Person p; // 整体定义
return 0;
}
如上,类当中的变量只是声明,他们是向上述一样整体进行定义的。
类的实例化
int main()
{
Person p; // 整体定义
return 0;
}
如我们之前写的这个例子,这就是一个类的实例化对象。
打个比方,我们要建房子,而类就像是一个 图纸一样,而图纸里面是不能建房子,住人的,他是一个模板,我们根据这个图纸就可以 开始建房子,而根据这个图纸建出来的房子就是这个类的实例化对象。这个对象才开辟了空间。
int main()
{
Person p;
p::age = 0; // error
return 0;
}
像这样是不行的,因为我们这样是在访问 类当中的 top,而类当中的top是没有开辟空间的,也就是说图纸当中的 是不能住人的。
对象的大小
以前在C当中计算结构体的大小很好计算,里面都是变量,只要注意内存对齐就行了,但是C++类当中的 不仅仅有成员,还有 成员函数,这怎么计算呢?
计算类的大小也是要进行 内存对齐的,而其中的成员函数是不进行计算的,只算成员的大小。
因为对象当中只存储了 成员,没有存储成员函数
这就好比是,成员是一个家,成员函数是篮球场,健身房,这些东西没有必要在家里来建造,都建造的话很浪费,用公用就行了。
此处的函数都是放在 一个 公共的区域,每一个这个类的对象,有的成员函数都在这个公共的区域里面。这个公共的区域编译器是知道在哪的,所以对象中不需要存储函数的地址。
class A
{
public:
void printA()
{
printf("%d", _aff);
}
//private:
int _aff = 10;
};
int main()
{
A a;
cout << sizeof(A) << endl; //4
cout << sizeof(a) << endl; //4
a._aff = 1; // 在 a 这个对象当中去访问
a.printA(); // 在公共区域当中去访问
}
需要注意的是:如果这个类是空类,也就是没有成员变量的类,那么这个类的大小是 1 byte,这个字节是为了占位,表示这个对象的存在。
class A
{
public:
void func()
{
}
};
class B
{
};
int main()
{
cout << sizeof(A) << endl; //1
cout << sizeof(B) << endl; //1
return 0;
}
内存对齐
- 1. 第一个成员在与结构体偏移量为0的地址处。
- 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
- 3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么要内存对齐
1) 平台移植:
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2) 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总体来说:结构体的内存对齐是拿空间来换取时间的做法。
第一个 _a 只读了一次,而第二次 不对齐的情况, 读了 两次
使用 #pragme pack(8)可以设置 默认对齐数为 8 ,在VS当中,默认对齐数是8。
在使用 #pragme pack(8) 之后,再使用 #pragme pack(),可以取消设置的默认对其书,还原默认。
this指针
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);//2022-1-11
d2.Init(2022, 1, 12);//2022-1-12
d1.Print();
d2.Print();
return 0;
}
先来看这个例子,既然成员函数都是调用的是一个 公共的函数,那么为什么此处的两个对象 d1 和 d2 ,两个对象调用的同一个函数,打印的结果不一样呢?我们看函数也没有传参,此处的是怎么分辨Print()函数中 _year _month _day 这些变量的?
此处,我们看来是访问的是 本 对象当中的 变量:
其实不是,因为这个只是一个声明,是没有存储空间的,其实这样的代码,编译器是会做一些处理的:
此处其实隐含了一个 this 指针,如果交给编译器,编译器会这样把这个函数进行修改:
// 编译器对成员函数的修改
void Print(Date* this)
{
cout <<this-> _year << "-" << this->_month << "-" << this->_day << endl;
}
然后再函数调用的时候,也会传入这个对象的地址:
Print(&A):
Print(&B):
但是上述只是编译器来写的,它在函数的形参和实参上面,只允许自己来创建 Date* this 这个形参,而不允许用户去 创建这个形参:
如上图,报错了。
但是编译器又允许我们在函数内部去使用这个this:
也就是说,这个this是编译器给我们创建好了的,我们可以在函数当中去使用这个this。
这个this 是不能修改的:
因为,其实此处的this 的类型这样的 : Date* const this ; 也就是说,此处的 const 修饰但是this ,这个this 是不能被修改的,但是 this 指向的内容是可以进行修改的。
const 加在 * 的前面,这个指针指向的内容才不能被修改。
空对象中的this
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); //Print()
return 0;
}
上述例子是正常执行的,但是下述代码不是正常执行的:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
运行错误:
首先一样的,创建了一个 p 空对象,然后对空对象 P 当中的print()函数进行访问,在Print()函数调用的时候,会把 p 的地址传进去,传递一个空指针不会报错。
第一种不会报错是因为,没有对空对象当中的成员进行访问,他访问的成员函数,成员函数是存储在 公共区当中的,不是在对象当中,不会发生解引用。
而在第二种当中,Print()函数对空对象当中的 _a 成员变量进行了访问,然后函数中就会把 _a这个变量之前加一个 this-> 表示引用,这时候的引用就是空指针引用,就会报错。
而上述 p->Print() 并没有解引用,因为 对象当中不存储 成员函数,成员函数存储在公共区当中,此处只是在公共区当中去找到这个函数,然后 call (调用)这个函数。
当然,使用 " :: " 来替代 " -> " 来使用也是不行的,因为 " :: " 前应该写的是 作用域,而类域当中的 成员变量和成员函数都是没有存储在其中的, 类只是一个模板,创建某一种对象的模板。
this 存储 在 哪里?(对象里面?栈?堆?静态区?常量区?)
首先不能在对象里面,因为之前我们在计算对象的大小的时候,是没有这this指针的大小的。
因为这个this 是作为函数的参数传入进去的,那么这个this 就要 压栈,那么这个this 指针是更普通参数引用 存储在函数栈帧当中的,也就是存储在栈上的。
在VS下,因为this要频繁的进行调用,所以VS对这个this 存储的位置进行了优化:
他把 this 指针存储在了 ecx 当中,ecx是寄存器。
C语言和C++实现Stack的对比
C语言实现:
- 每个函数的参数都是 Stack* ,都是通过指针来操作的,调用时候,必须传入Stack指针。
- 每个函数第一步都需要判断,传入函数的 Stack* 等等这些参数是否为NULL。
而且在主函数当中调用这些函数的时候,命名都是 数据结构的类型 + 函数操作:
我们发现,C当中实现 Stack 都需要用指针来实现,在链表当中有得地方甚至要用要 二级指针,这样不管是在实现,还是在 使用的时候,都很容易出错。
C++实现:
C++当中 是把 Stack 当中的 函数和 结构的变量定义到一起,如果把成员的访问权限打开,那么对于像 栈,顺序表这些甚至可以直接访问创建对象的当中的成员。在类当中,我们可对 类当中的方法和 成员,根据 我们想不想要用去去访问,利用访问权限进行封装:
而且在主函数当中去 使用其中的方法的时候,显得很方便,不用再去 复杂的命名,而且 调用的时候不再使用指针:
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}