c++详解之类和对象——类的定义,存储方式,this指针!
面向过程和面向对象初步认识
面向过程:分析出求解问题的步骤,通过函数调用逐步解决问题
面向对象:关注的是对象!讲一件事分成不同的对象,靠对象的交互之间的完成的!(至于怎么实现就不用管)
例如我们设置一个外卖系统:我们关注如何点餐,如何送,如何......(面向过程
面向对象:我们分成了 商家,商品,用户,骑手这几个对象!
关注的是模块!和模块之间的关系
类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现==在以C++方式实现, 会发现struct中也可以定义函数==,类就是结构体的升级版!
以前的结构体我要定义的骑手我只能定义它的各种属性!但是没有方法!
但是骑手也应该要有各种的动作,比如接单,不接单,结单....这些都被归为一类!
类兼容c中struct的所有用法!
进行了升级可以定义函数!
以前我们定义一个stack的结构体,实现它的功能还要把他的名字也加上!
比如stackIint....不能直接定义成Iint否则不止要初始化谁!
但是我们使用类的话这些函数都是在类里面
我们直接调用里面的Iint(里边的成员函数/方法)(方法就是函数!)
而且以后也不用加struct!类名可以直接当做类型!
后面的话用class来当初类的关键词!但是struct也是可以的,因为已经被升级为类了!
//这是c语言的用法! typedef struct stack { int* a; int size; int capacity; } stack; void InitStack(stack* ps) { //...... } //在c++中我们可以直接 struct stack { int* a; int size; int capacity; }; void InitStack(stack* ps) { //...... } //因为c++的struct已经是类的,所以可以直接使用当做类型!
封装
面向对象三大特性:封装,继承,多态,不是说只有这三种,而是说这三种最出名
封装是一种管理!
封装是相比c语言,c语言不进行封装,体现在c++的类!
在c语言中数据是数据!方法是方法!很*!
而c++认为这样的*是不好的,于是有了类——把数据和方法放到一起!这就有了类
增删查改都只能通过共有成员!一般都不让人进行直接访问数据!
因为在c语言中有人可能会通过不规范方式对数据进行访问!
我们既可以使用接口访问,也可以直接访问!但是不规范的访问可能出问题!
top不一定是栈顶的值,可能是top-1看初始化!
而c++引入了类就没有这种问题!
这是一种更好的管理行为!
因为当使用的人多了,可能就会出现这种行为!
所以才要进行封装!
就像是一个景点,为了防止被破坏,就要有规矩让参观者进行参观!有限度的进行参观!
但是维护者可以去直接接触
类的访问限定符!
c++为了更好的封装使用了访问限定符!
访问限定符可以让我们允许类里面的内容是否被别人访问,也可以更好的保护数据
在c语言中,我们虽然可以使用接口去访问增删查该结构体里面的数据,但是我们也是可以直接的对结构体里面的数据进行访问!这对于一个公共大型项目来说是很不好的!不能保证有谁不会乱修改
c++里面我们使用了访问限定符,对于我们不愿被别人直接修改的数据,我们直接不允许进行访问,只可以调用接口去对数据进行!
//其实struct也是可以的,不过我们一般都是使用class!
//class里面默认是私有的!
//struct里面默认是共有的!
class stack
{
private://私有部分——只能在类里面访问
int* _a;
int _size;
int _capacity;
public://共有部分——类的内外都可以访问
void Init(stack* st)
{
_a = NULL;
_capcity = 0;
_size = 0;
}
void Push(stack* st)
{
//.....
}
};
//在类中数据类型不分上下可以在上面也可以在下面!不像C语言中一样一定要先在上面声明定义!
class stack
{
public:
void Init(stack* st)
{
_a = NULL;
_capcity = 0;
_size = 0;
}
void Push(stack* st)
{
//.....
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
stack ST;
//ST._a;报错!
}
访问限定符总结总结
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类的作用域
每一个{}都是一个类,命名空间有命名空间的作用域,类也有类的作用域,叫类域
class student
{
public:
void printstduent();
private:
int _age;
char _name[20];
int _id;
};
//我们要指定printstudent是属于student这个类域里面的!
//因为不同的类域可能有相同名字的成员函数!
void student::printstduent()
{
cout << _name << " " << _id<< " " << _age << endl;
}
class test
{
public:
int _a;
int _b;
int test_(int c, int d)
{
cout << c << " " << d;
}
};
int main()
{
//既然它是一个作用域那么我们能不呢像命名空间一样用域作用限定符来访问呢?
test:: _a = 10;//不能
test:: test_(10,20);//不能
//原因我们后面解释
}
类的实例化
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没 有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
class test
{
public:
int _a;
int _b;
int test_(int c, int d)
{
cout << c << " " << d;
}
};//这个类定义没有占用实际内存空间
//相当于一个图纸
int main()
{
test a;//类的实例化!——相当于将图纸里面的东西给实现出来,这才占用了内存空间!
test b;
test c;
//....
//回到上面的 test:: _a = 10;
//因为没有实例化!根本没有占用内存空间!所以也就无法进行定义,修改,赋值!
//命名空间能够访问是因为命名空间只是个已经存在的东西加了一堵墙!
return 0;
}
类和对象的存储方式
你觉得类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算 一个类的大小?
类应该如何存储才是最高效的呢?
下面我们看看一下的几种存储办法——分析一下
使用存储方法一,就会造成巨大的空间浪费!
因为我们知道,每个类的函数其实都是一样的,如果对每个类的函数都存放在类里面那么岂不是会占用很多的内存空间?
就像是一个小区,锻炼器材都是公共的,但是卫生间什么的是独立的!
锻炼器材如果每家都一个就太浪费了
存储类型二,我们只保存类成员变量和函数表的地址!将成员函数统一放在一个地方,需要的时候,根据地址找到函数进行调用!
二相当于在小区的某个地方放了公共锻炼器材,我们可以根据地图找到这些公共器材去使用,但是这样未免也太麻烦了?既然是公共使用的为什么不直接放在显眼的地方,然后直接就能找到呢?
存储类型三,就完美的解决了上面两种存储方式的缺点
**公共代码区也叫常量区!**这个位置是编译器放的,编译器知道放在哪,所以就不用去定位!也就不用放指针了!
//我们也可以进行测试 // 类中既有成员变量,又有成员函数 class A1 { public: void f1(){} private: int _a; }; // 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 {}; int main() { cout << "A1:" << sizeof(A1) << " " << "A2:" << sizeof(A2) << " " << "A3:" << sizeof(A3) << endl; return 0; }
class A { public: void PrintA() { cout << _a << endl; } //private: char _a; }; int main() { A aa1; A aa2; A aa3; aa1._a = 1; aa1.PrintA(); aa2._a = 2; aa2.PrintA(); aa3._a = 2; aa3.PrintA(); //三个数据是独立存储的! //而函数是相同的! }
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
分配一个字节就是要说明这个对象的存在!不然如果是零字节就无法存在分辨对象了!
==其实当类被定义的时候,只是其成员变量没有去占用内存空间,而成员函数此时已经被定义好然后放在公共代码段了!==
class test { public: int _a; int _b; int test_(int c, int d) { cout << c << " " << d; } }; int main() { test:: test_(10,20);//那既然如此为什么这个代码运行的时候仍然会报错呢? //成员变量是因为没有实例化而没有占用空间,但是为什么这个成员函数已经定义了却仍然无法使用? }
结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
- .结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
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()
{
Data d1;
d1.Init(2002,9,21);
Data d2;
d2.Init(2002,9,22);
d1.Print();
d2.Print();
}
答案是编译器对类里面的==非静态成员函数==进行了一次处理
我们看着是没有参数的!但是其实它被添加上了一个隐形参数 这个参数就是this指针!
this是一个关键字!不能被修改,类型就是定义的类,而且它就是==第一个参数!==
在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
//编译器修改后
void Print(Date* const this)//添加this指针是编译器的工作,我们不可以去自己添加!
{
cout <<this->_year<< "-" <<this->_month << "-"<< this->_day <<endl;
}
//this* const 为了防止我们把指针改了!const用于保护指针本身不被修改!
//所以对象进行调用的时候其实就是一个传参的过程!
d1.Print(); // = Print(&d1);
d2.Print();// = Print(&d2);
//但是我们不可以主动去传参
d1.Print(&d1);
d2.Print(&d2);
//这样写是不行的!这是编译器的工作!
//这样就最终回答了上面的疑问
//test:: test_(10,20);这个无法直接调用的原因是因为缺少了一个this指针参数!但是我们又没有办法去传,这就导致了报错!
但是我们可以在类里面使用this指针!
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this << endl;//当调用的时候我们可以看出d1,d2的地址
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
//我们也可以手动的添加上this指针的解引用!但是我们一般不怎么做,因为编译器会自动添加上!
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Data d1;
Data d2;
d1.Print();
d2.Print();
}
this指针的特性
-
this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
-
只能在“成员函数”的内部使用
-
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针
因为this指针是形参所以this指针也是存在栈帧中的!
-
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递
this指针可以为空吗?
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
答案是==正常运行!==
因为我们上面说过!
p->Print();//会在编译器下变成 // print(p);变成一个传参的过程,并没有发生空指针的解引用! //print本身也在公共代码区
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
答案是运行崩溃
p->PrintA();//变成printA(P);运行正常 void PrintA() { cout << _a << endl; } //编译器处理过后 void PrintA(A* const this) { cout << this->_a << endl;//发生了this指针的解引用!也就是是发生了空指针的解引用 }