@[TOC]
前言:
认识面向过程与面向对象
- C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题 。
- C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成 。另外由于C++==完全兼容==C,因此C++又可以称为是==面向过程与对象的高级语言==。
封装
- 对于软件设计而言,为了尽量避免某个模块的行为干扰到同一系统中的其它模块,应该让模块仅仅公开必须让外界知道的内容,而隐藏其它一切内容。--即将数据(成员变量)和操作数据的方法(成员函数)进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
- 封装的目的:方便管理,毕竟自由是要付出代价额。反面教材:倡导自由民主的某国停止更新全国疫情。
浅谈C的结构体与C++类
因为C++完全兼容C,因此C的结构体也可以在C++编译器器下正常使用。同时C++扩充了C的==结构体==:增加访问限定符,封装,继承,多态等。==因此C++中完全可以将结构体当做类来使用==,只是结构体成员默认是==公开==的,而C++中的类默认是==私有==的。 但是为了方便区别C与C++,C++中更喜欢用==class来==表示struct。
类与对象–上篇
class的引入
- 整个世界都可看作是由具有行为的各种对象组成,同一类对象都具有相同的的行为与属性。
- 比如人:人都有姓名,年龄,体重等相同的东西---其实就是所谓的属性,而人的吃饭,睡觉等动作就是所谓的行为。而C++抽离出了这些相同的行为与属性--将其定义为==类 *类型==,同时根据类类型,定义一个具体的对象
ps:
- 很明显类是一个抽象的定义,需要自己慢慢体会其中的区别。
类的定义
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
- class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
- 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理—==内联函数对于编译器只是一种建议,具体是否当做内联函数处理,是看编译器的,不同编译器,要求不同==。
- 声明在外,定义在外,只是这里需要用到 ==作用域限定符“::”==,来表明该函数属于哪个类域.—-==类其实定义一个新的域,类似文件域,命名空间域等==。
类中的限定符
- C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
- 类的限定符有3种:
private(私有)
声明为私有的成员(无论是成员变量是成员函数),不能通过对象直接访问,但是在成员函数中可以直接访问。protected(保护)
保护类成员各更多的用于继承,目前博主未学到,后续会补充。public(公有)
公有的成员(无论是成员变量是成员函数),可以通过对象进行直接的访问,当然成员函数也是可以的。
- 形式
- 问题:访问限定符,会对数据有影响吗?
无任何影响,访问限定符只是在编译阶段告诉编译器,该成员是private,protect还是public的,一但数据存放到内存后,就不在有区别了,但是对这块内存的访问权限是有限定的。
类的实例化
- 用类类型==创建对象==,同时也是对象==成员变量的定义==地方,称为类的实例化 ---注意类类型是一种声明,其==成员变量并未定义即分配内存==,只有当创建一个具体的对象(实例化一个对象时),成员变量才分配内存同时初始化。
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
- 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例出的对象才能实际存储数据,占用物理空间
如何计算类的大小
- 问题:类的计算大小仍然是使用C语言中结构体的内存对齐方法,但是C中的结构体是没有成员函数的,而类中既可以有成员变量,又可以有成员函数,那么怎么计算呢?
- 每个对象中成员变量是不同的,但是==调用同一份函数==,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间 .
- 为了解决这个问题:C++中将类中的成员函数放在了共同的区域即
只保存成员变量,成员函数存放在公共的代码段
- 因此在计算类大小时,只需要计算成员变量的大小,==规则仍是C结构体的内存对齐规则==:详细见博客:结构体
隐形的this指针!!!!非常重要
this指针的引入
- 前面介绍了,成员函数是存放在公用的==代码段==,那么如果多个对象调用同一个成员函数,成员函数是如何识别传给其的是那个对象的数据呢?
- C++中通过引入==this指针==解决该问题,即:C++编译器给每个==非静态的成员函数==增加了一个
隐藏的指针参数
,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该
指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特点
- this指针的类型:类类型 const 即指针常量(类名 + +const +this),可以更改this指针指向的对象的数据,但是不能更改this指针的指向,这里要小心权限问题,不要将常对象的地址传给this。
- 只能在==非静态成员函数==的内部使用 —关于静态在下面详细介绍
- this指针本质上其实是一个==成员函数的形参==,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是成员函数==第一个隐含的指针形参==,既然作为形参this就存放在了栈区,但是因为this经常的使用,编译器==也会==通过ecx寄存器自动传递,不需要用户传递。
this指针面试题---考察对空指针的理解
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();//没有错误
p->PrintA();//错在this—>_a
}
- 地址为o的那块内存,因为作为内核代码区,里面有重要的数据,==因此不可访问,不可读写,但是可以进行比较,计算==。
- 当给一个指针赋值为nullptr时,编译器会做==隐形的类型==转换处理
ps:(A*)nullptr.因此给this指针赋值nullptr是没问题的,问题出在Pint函数中我们通过this指针去访问了nullptr这块内核代码区,这是非法,不允许的。
因此程序在Print出问题。
类与对象--中篇
对于任何一个类A,都含有4个==自动生成==默认的成员函数和2个重载的操作符&,==前提是没有显示定义这些函数==:
- 用于==对象创建时初始化==的2个:默认构造函数与默认拷贝构造函数(是一种浅拷贝)---==一般不期望用自动生成的构造函数==
- 用于==已存在对象间==的赋值函数1个:默认赋值函数
- 用于清除对象的一个:析构函数,==一般不期望用自动生成的析构函数==
- 用于==取对象和常对象地址==的2个&的重载运算符:&与const&
下面依次介绍。
构造函数与析构函数
背景
当我们在C项目,可能因为未初始化对象内存导致的程序==CREASH==,也可能因为忘记销毁对象导致==内存泄漏==,根据经验,大部分是因为我们经常忘记导致,因此C++之父 Bjarne Stroustrup在设计C++语言时,充分考虑到了这个问题并很好的解决。
把对象的初始化工作放在 构造函数中,把清除工作放在析构函数中。当创建对象时,构造函数被自动执行;对象消亡时,析构函数被自动执行,这样就不用担心忘记对象的初始与清除工作。
命名方式
构造函数与析构函数的名字不能随便起,必须让编译器认出,才能被自动执行。Bjarne Stroustrup的命名方法即简单又合理
让构造函数,析构函数与类同名,由于析构函数的目的与构造函数的目的相反,就加前缀“~”,以示区别(“ ~”本身就有取发的意思),且没有返回值
构造函数的特性!!!!!!!
- 构造函数是一个==特殊的==成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的==初始值==,并且在对象的生命周期内只调用一次 .
- 构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个,但是通过函数重载,自定义的构造函数可以有多个,但是要能被编译器识别,准确调用。注意:无参构造函数、全缺省构造函数、都可以认为是默认成员函数 。==建议使用全缺省构造函数==
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 。另外编译器自动生成的无参构造函数是不会对内置类型的 进行任何操作的,但是会去调用那些自定义类型的成员变量的默认构造函数(无参,或者全缺省的构造函数),一但自定义类型中没有默认构造函数,程序会报错。---==不用期望使用编译器自动生成的默认的构造函数==。
- 构造函数可以重载 ,但是尽量不要同时定义一个全缺省的构造函数和一个没有缺省参数的构造函数,因为编译器在初始化阶段可能无法判断调用哪个构造函数
析构函数的特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。编译器生成的默认析构函数,对会自定类型成员调用它的析构函数 ,而默认的析构函数并不会产生任何效果。---==不用期望使用编译器自动生成的默认的构造函数==。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数 。
拷贝构造函数
- 构造函数:==只有单个形参(还有隐形的this指针)==,该形参是对本类类型对象的引用(一般常用const修饰,因为拷贝不需要改变对象数据),在用已存在的类类型对象创建新对象时由编译器自动调用
- 如果没有显示的定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数,只是它只会以==字节序的形式去一字一字==的将内存中的放到对象的内存中--这会出现很大的问题,---如果对象中含有动态开辟的数据类型,那么通过析构函数会导致同一快堆空间被释放了2次,这是非法的!
构造函数,拷贝构造函数,析构函数对比
- 当创建一个对象时,虽然空间已经开好了,但是内存中的数据是==随机的==,因此需要初始化一下。而构造函数与拷贝构造的区别是:
构造函数是用数据去初始化对象,拷贝构造函数是用一个已存在的对象去初始化对象。
- 对象调用构造函数的顺序
- 程序中==先定义的先调用==构造函数。
- 对于含有==自定义类型==的对象,先调用==自定义类型==的构造函数,如果自定义类型还有自定义类型就先调用该自定义类型的构造函数,---很明显这是一个==经典套娃问题==。
- 析构函数的顺序
- 因为局部变量都是定义在栈中,因此在销毁时,遵循“先创建后销毁”的特性,后定义的对象先调用析构函数,先定义的对象其次。
- 对于含有自定义类型的成员的对象,先析构该对象,再析构自定义类型。
赋值运算符重载
赋值与初始的区别
- 初始化:对象==创建的同时使用初值直接填充对象的内存单元==,因此不会有数据类型的转换等中间过程,也不会产生临时对象。
- 赋值:对象创建好后任何时候都可以调用的而且可以多次调用的函数,由于它调用的是“=”运算符,因此可能会有中间对象的生成,
运算符重载
- 编译器的内置的运算符只能用于内置类型(int char等),而对于自己定义类型怎么定义它的运算符呢?
- C++为了增强代码的可读性引入了运算符重载(也确实应当如此),运算符重载是具有特殊函数名的函数,也具有其==返回值类型==,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字为:关键字operator后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符==必须有一个类类型==或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
- 操作符有一个默认的形参this,限定为第一个形参
- “.*”, “::” 、”sizeof” 、“?:” 、 “.”注意以上5个运算符不能重载。这个经常在笔试选择题中出现
赋值运算符重载
- 赋值运算符的重载,更多是使用引用做参数,一方面不用担心中间变量的生成,另外一方面传参更快。
那么上手一个项目吧:Date的实现--运算符重载经常被使用
const 成员函数
- 我们知道 this指针是 指针常量--不可更改其指向,可改其指向对象的数据。那么如果我们定义一个常属性的对象,还能正常使用this---肯定是不行的,由只读到可写可读是权限的放大。
- 那么该应该解决这个问题呢?改变this指针的属性就行了
一般我们在函数后面加一个==const==,这样this就变成了 const A* const this权限放小是没问题了
- 将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
关于const成员函数的一些问题
const对象可以调用非const成员函数吗?
不行 ,const A 与 A const this不匹配,有权限放大问题非const对象可以调用const成员函数吗?
可以,A 与 A const this 权限的放小。不过如果再从const成员函数中只能调用const成员函数。
const成员函数内可以调用其它的非const成员函数吗?
不行,我们知道调用函数其实是给this指针转抵地址。但是 const 成员中的 this被修饰为 const A*const this,
而非const 成员函数的this是A *const this,如果const成员函数传给非const就成为==权限的放大,这是非法==。
非const成员函数内可以调用其它的const成员函数吗?
可以同样道理,只是权限的放小,是没问题的。
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,用编译器默认会生成的就行。
class Date { public : Date* operator&() { return this ; } const Date* operator&()const { return this ; } private : int _year ; // 年 int _month ; // 月 int _day ; // 日 }
类和对象--下篇
再谈构造函数
我们通过构造函数的==函数体==,本质上是对成员变量的一种==赋值==,而赋值说明成员变量在进入构造函数体前==就已经存在==了。而 我们知道具有常属性的变量,它只有一次初始化的机会,后续也不能通过赋值等操作,那么构造函数那种函数体赋值就是非法的。 ---C++提高了解决该问题的方法:初始化成员列表
初始化成员列表
定义
初始化列表:以一个 冒号开始,接着是一个以 逗号分隔的数据成员列表,每个" 成员变量"后面跟一个放在括号中的初始值或表达式。
初始成员列表的特性
- 前面分析:本质上初始化成员列表 是==成员变量初始化==的地方,这就很好的解决了常属性变量的 ==初始化问题==。
- 对于含有 以下3种成员变量的类,构造函数时一定要使用 初始化列表
- 具有引用的变量---引用定义时必须初始化
- const 成员变量---定义时必须初始化
- 自定义类型成员(没有默认构造函数的)---显示的构造函数,在构建对象时,必须给定初值以初始化。
- 尽量使用初始化列表初始化成员变量,也当如此,无论何时它都不会想构造函数的函数体那样容易犯错。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
,举个例子
static
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。一但声明为static,它的生命周期只到程序结束为止。
static修饰成员变量
- 静态成员变量和类的普通成员变量一样,也有public、protected、private3种访问级别。
- 静态成员为所有类对象所==共享==,不属于某个具体的实例,
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 建议为一个变量只匹配一个关键字修饰,否则会出现权限问题。
static修饰成员函数
- 静态成员函数和类的普通成员函数一样,也有public、protected、private3种访问级别,也有返回值
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
要想访问非静态成员,必然需要给this传值,可惜不行。如果真的想调用非静态成员,可以定义一个类对象的形参,这样静态成员函数就变成了与普通成员函数一样,可以访问类中的任意成员。---当然也不建议怎么做,破坏了静态函数不能访问非静态函数的属性。
- 静态成员函数可以直接访问静态成员变量与静态成员函数
4. 类静态==成员函数==即可用类名::静态==成员函数==或者对象.静态成员函数来访问,,因此非静态函数可以访问任意的成员
静态成员与非静态成员函数比较
- 静态函数没有this,因此不能访问非静态函数,但是可以访问静态成员变量与成员函数
- 非静态成员函数 可以随便的访问非静态与静态成员。
C++11初始化变量的新方法
C++11支持==非静态成员变量==在声明时进行初始化赋值 但是要注意这里不是初始化,这里是给声明的成员变量缺省值 , 是一种针对编译器自动生成的默认构造函数不对成员变量不做任何处理的 ‘’向上兼容‘’,也是一种类型提前赋值的方式。
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用 ,具有友元关系的有3种:友元函数,友元类,内部类
友元函数
如果我们想在一个==非成员函数的函数===访问 类的私有成员,只需要在类中声明一个frend函数,就可以正常使用类中的私有成员。
定义
友元函数的特性
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字 。
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
友元类(小心成为舔狗)
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行---舔狗。即B是A的友元,A是舔狗- 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元
内部类
- 概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它==不属于外部类,更不能通过外部类的对象去调用内部类。==外部类对内部类==没有任何优越的访问权限==。
- 注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友 .
总结
C++的语法细节很多,多练多思考会有帮助吧!