Welcome to 9ilk's Code World
(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 与C++的邂逅
本篇博客将讲解C++中的类和对象,C++是面向对象的语言,面向对象三大特性是封装,继承,多态。学习类和对象,我们可以很好的认识到封装这一层。
本篇内容思维导图:
🏠 面向过程和面向对象初步认识
- C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。面向对象即将一件事分为好几个过程。
- C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。面向对象即关注对象以及对象之间的互动。上图中总共有 四个对象:人,衣服,洗衣粉,洗衣机。
整个洗衣服过程主要是: 人,衣服、洗衣粉、洗衣机四个对象之间交互完成的,人不需要关
新洗衣机具体是如何洗衣服的,是如何甩干的。
🏠 类的引入
typedef int DataType;
struct Stack
{DataType Top(){return _array[_size - 1];}DataType* _array;size_t _capacity;size_t _size;
};
C++中的struct与C语言中的不同:
- C++中struct兼容了C语言中struct的用法,但同时将它升级成了类。
- struct在C++中可以定义变量(成员变量),也可以定义函数(成员函数),且定义的函数不需要传参就可以使用在struct中定义的变量。
- 升级之后的类可以直接用struct名称来代表类型。一个类对应一个自定义类型,那么他就可以创建出N个变量,也就是能实例化出N个对象。
struct ListNode
{int _data;
}struct List
{ListNode* next; //兼容之前struct
}int main()
{struct List l1; List l2; //直接用struct名称定义对象 return 0;
}
但是上面结构体的定义,C++中还是喜欢用class来代替。
🏠 类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
- class为定义类的限定符,className为类名,{ }中为类的主体,注意定义类结束时后面分号不能省略。
- 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
📌 成员变量命名规则的建议:
class Date
{
public:void Init(int year){// 这里的year到底是成员变量,还是函数形参?year = year;}
private:int year;
};
这段代码是可以正常编译的,但是定义的成员变量不能初始化成功,因为编译器不知道这里的year到底是成员变量还是函数形参。我们建议将成员变量采用一些前缀命名来以示区分。
class Date
{
public:void Init(int year){_year = year;}
private:int _year;
};// 或者这样
class Date
{
public:void Init(int year){mYear = year;}
private:int mYear;
};
📌 类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Person
{public:
//显示基本信息
void showlnfo()
{cout <<_name<< "-" <<_sex << "-" << _age << endl;
}public:char* _name;char* _sex;int _age;
};
- 类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中,注意: 成员函数名前需要加类名 :: .
// Person.cpp
class Person
{public:
//显示基本信息
void showlnfo(); //声明
public:char* _name;char* _sex;int _age;
};//Person.h
void Person::showlnfo()
{cout <<_name<< "-" <<_sex << "-" << _age << endl;
}
在这里类形成了一个类域,在.h文件时,先局部域再全局域搜索showlnfo,此时两个域都没有找到这个函数,因此只有指定类域在类域里面找才能找到。
🏠 类的访问限定符及封装
📌 访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过 访问权限选择性的 将其接口提供给外部的用户使用。
- public修饰的成员在类外可以直接被访问,protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的).
class A
{
public:void f(){cout << "f()" << endl; }
private:int _a;
}int main()
{A aa;aa.f(); //成员函数是公有可以在类外直接访问cout << aa._a << endl; //错误 成员函数是私有在类外不能直接访问。return 0;
}
- 虽然被private修饰的成员不能在类外直接访问,但是能在类外通过其他公有成员间接访问。
class A
{
public:void print(){cout << _a << endl; }
private:int _a;
}int main()
{A aa;cout << aa._a << endl;return 0;
}
- class的默认访问权限为private,struct为public(因为struct要兼容C)
class A
{int _a; //此时a是被private修饰的
}struct B
{int _b; //此时b是被public修饰的
}
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;如果后面没有访问限定符,作用域就到 } 即类结束。
class Date
{//默认就是private 此时Init在类外不能直接访问void Init(int year){_year = year;}
public:void func() //此时public作用域从它出现位置到private{}private:int _year;
};
- C++类中常将成员变量的访问权限设置为private,成员函数设置为public,此时成员变量经过封装只能通过公共渠道也就是成员函数访问,保证了数据的安全性。
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别,在语法层上限制你对成员的使用而不是影响成员所在位置。
1.C++需要兼容C语言,所以C++中struct可以当成结构体使用。2.C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。3.在继承和模板参数列表位置,struct和class也有区别。
📌 封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
- 封装本质上是一种管理,让用户更方便使用类。在类这里,将数据和方法放进类里这是第一层封装;类里面又使用了访问限定符这是第二层封装。
例子:比如西安的景点兵马俑并不是随意让游客触摸的,是有着一定保护的,如果没有保护就相当于是C语言结构体成员直接放开,用围栏围住相当于是将兵马俑(成员)保护起来,方便进行管理。
- 在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
🏠 类的作用域
类定义了一个新的作用域 ,类的所有成员都在类的作用域中 。 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。类域是为了防止类域中与其他域同名成员产生冲突。
class Person
{
public:void PrintPersonInfo();
private:char _name[20];char _gender[3];int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{cout << _name << " "<< _gender << " " << _age << endl;
}
🏠 类的实例化
用类类型创建对象的过程,称为类的实例化.
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;就像C语言我们定义这个结构体类型时,只是对他的一个声明,实际并未开空间。
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。就像C语言中我们用结构体类型定义变量时内存是实际分配物理空间给这个变量的。
- 类和对象是一对多的关系。类好比现实中工程师设计的建筑图纸,而对象类似根据图纸建造出来的建筑(是实际分配空间的)。
🏠 类的对象模型
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
📌 类对象的存储方式猜测
- 对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时, 每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
- 代码只保存一份,在对象中保存存放代码的地址
- 只保存成员变量,成员函数存放在公共的代码段
📌计算类的大小
// 类中既有成员变量,又有成员函数
class A1
{
public:void f1(){}
private:int _a;
};// 类中仅有成员函数
class A2
{
public:void f2() {}
};// 类中什么都没有---空类
class A3
{};int main()
{cout << sizeof(A1) << endl;cout << sizeof(A2) << endl;cout << sizeof(A3) << endl;return 0;
}
注:既可以sizeof()实例化出来的对象,也可以sizeof()类,好比我们可以根据设计图纸推测出建造所需要的空间。
输出结果:
4 //A1
1 //A2
1 //A3
由此我们可以得到以下结论:
- 一个类的大小,实际就是该类中 ” 成员变量 ” 之和,当然要注意内存对齐 。
- 类的对象模型应该对应的是猜测三,也就是 成员函数放在公共代码段,因为如果每个对象放一份会造成大大的浪费。
- 对于空类占一个字节,这一个字节不存储有效数据,而是用来标识对象被定义出来了。
- 对于嵌套类类比嵌套结构体
class A1
{
public:char _c;int _a;
};//对齐到8class A2
{
public:long long _l; //8A1 _aa;//_aa最大对齐数为最大成员也就是int的对齐数,大小是这个类大小
};int main()
{cout << sizeof(A1) << endl; //8cout << sizeof(A2); //16return 0;
}
📌 结构体内存对齐规则
1. 第一个成员在与结构体偏移量为 0 的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处.注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值.VS中默认的对齐数为 83. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己成员中最大对齐数的整数倍处,所占大小就是这个嵌套结构体大小,最后结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
答:结构体对齐规则如上。1. 平台原因 (移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的 ;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问; ⽽对⻬的内存访问仅需要⼀次访问 。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值 了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。总的来说,内存对齐是以空间换时间的做法。
1.#pragma 这个预处理指令,可以改变编译器的默认对⻬数。2.若进行任意对齐,由于大多数处理器每次从内存读取2的倍数个字节,此时若不是2的倍数,数据一多起来就有可能降低效率。因此不能按任意字节对齐。
1.超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储。a.⼤端(存储)模式:是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。b. ⼩端(存储)模式:是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存 在内存的⾼地址处。2. 对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤ 于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题,此时需要考虑大小端。
🏠 类成员函数的this指针
📌 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);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}
对于上述这样的一个日期类,用Date类实例化出来d1和d2两个对象,Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?C++中通过引入this指针来解决这个问题.
C++ 编译器给每个 “ 非静态的成员函数 “ 增加了一个 隐藏的指针参数 ,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 “ 成员变量 ” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递, 编译器自动完成 。
📌 this指针的特性
- this指针的类型时 类类型 * const,即this指针不能被赋值,但是能被强转.
class A1
{
public:void Print(){//this = nullptr; //不能被赋值cout << typeid(this).name() << endl;//cout <<(A1*)this->_a << endl;cout << typeid((A1*)this).name();}char _c;int _a;
};
- this指针只能在“成员函数”的内部使用.
- this指针本质是"成员函数"的形参,当对象调用成员函数时,会将对象的地址作为实参传给this形参,this指针在形参和实参不能显示写由编译器处理.
void Print(){cout << _a << endl;}//编译器处理
void Print(Date* const this)
{cout << this->_a <<endl;
}
- 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; }// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public:void PrintA() {cout<<_a<<endl;} private:int _a; }; int main() {A* p = nullptr;p->PrintA();return 0; }
对于上面这两段代码,都是将A类型指针赋值为nullptr,当p->PrintA时,并不是解引用空指针,本质是将nullptr传递给成员函数的形参this指针,而且成员函数并未存在对象里,,所以对于代码一能正常运行;对于代码二访问了this指针指向内容此时是空指针解引用因此会崩溃.值得注意的是,如果解引用空指针并不会编译报错,因为不是语法层的错误.
我们从汇编层就可以看到问题所在。
【面试题】1. this 指针存在哪里?this指针本质是成员函数的形参,我们知道函数形参是存在栈的,因此this指针存在栈区,但是有的编译器会优化直接存在ecx寄存器里面。但是注意的是,this指针不可能存在对象里,空类大小为1就可以证明。2. this 指针可以为空吗?this指针可以为空,但是注意空指针的解引用问题,而且这里是对象指针初始化为空,并不意味着接下来可以给他赋值为空。
本节我们初步认识了类中的成员们以及一些细节比如访问限定符,this指针等,夏姐我们将讲解有关类的默认成员函数。