C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。前面的数据结构之类的,需要把每一个步骤都做好,如初始化、插入、删除和销毁。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。例如:struct Stack { void Init(size_t capacity) { //TODO } void Push(const int& data) { //TODO } int Pop() { //TODO } void Destroy() { //TODO } int* _array; size_t _capacity; size_t _size; }; int main() { Stack s; s.Init(10); s.Push(1); s.Pop(); s.Destroy(); return 0; }
上面结构体的定义,在C++中更喜欢用class来代替,直接把struct换成class就可以了。
class ClassName { //类中由成员函数和成员变量组成 }; //一样需要加分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。类的两种定义方式:1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。例如上面的Stack的演示案例就是这样的。2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名:://.h文件 struct Stack { void Init(size_t capacity); void Push(const int& data); int Pop(); void Destroy(); int* _array; size_t _capacity; size_t _size; }; //.cpp文件 void Stack::Init() { //TODO }
一般情况下,更期望采用第二种方式。给成员变量命名的时候可以像示例代码一样,在变量名前加一个"_",像这样有便于区分是成员变量还是函数参数。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。访问限定符有:public(公有)、protected(保护)、private(私有)1. public修饰的成员在类外可以直接被访问2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class的默认访问权限为private,struct为public(因为struct要兼容C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
面向对象的三大特性:封装、继承、多态,先来说一下什么是封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
简单来说就是更好的让用户使用某种功能,比如电脑内置了复杂的硬件,但是用户平常使用的只是开机、关机、键盘等等,也不需要知道这些操作是怎样实现的。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符,指明成员属于哪个类域。class Person { public: void Print(); private: char _name[20]; char _gender[3]; int _age; }; // 这里需要指定Print是属于Person这个类域 void Person::Print() { cout << _name << " "<< _gender << " " << _age << endl; }
用类类型创建对象的过程,称为类的实例化1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
类的大小只需要算类中成员变量所占有的大小就够了,和结构体的大小计算是一样的,虽然类中除了成员变量,还有一部分就是成员函数,但是在设计的时候就把成员函数放在了公共代码区,所以要想计算类的大小,就计算成员变量的大小就可以了。结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
1. 第一个成员在与结构体偏移量为0的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为83. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
用下面这个日期的类来解释一下
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout <<_year<< "-" <<_month << "-"<< _day <
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。2. 只能在“成员函数”的内部使用3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
如果一个类中什么成员都没有,简称为空类。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数就是用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
概念:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。特性:构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载,提供多个构造函数,实现多个初始化方式。5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。6. 编译器默认生成的构造函数对于内置类型不处理,但是对于自定义类型来说就会调用自定义类型的构造函数,内置类型就是int,char等,自定义类型就是class实现的类。在C++11中针对内置类型不初始化的缺陷,打了一个补丁,内置类型成员变量在类中声明时可以给默认值。7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。class ClassName { public: ClassName() // 无参 {} ClassName(int n) // 有参 { _n = n; } private: int _n = 0; // 这里不是初始化,是给缺省值 };
默认构造函数:
1. 不写,编译器自动生成。
2. 自己写全缺省构造函数。
3. 自己写无参构造函数。
4. 不传参数就可以调用。
总结:
1. 一般的类都不会让编译器默认生成构造函数,都要自己写,显示写一个全缺省。
2. 特殊情况才会默认生成,比如类中有自定义类型的成员函数。
3. 构造函数频繁调用,直接写在类里面定义作为内联inline。
概念:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。特性:
1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。4. 对象生命周期结束时,C++编译系统系统自动调用析构函数,生命周期就是作用域。5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄漏。class ClassName { public: ~ClassName() {} private: int _n; };
默认生成析构函数:
1. 跟构造函数类似,内置类型不处理,自定义类型调用调用它的析构函数。
2. 一般不用自己写,有申请资源的时候才要写。
3. 先定义的对象后析构,后定义的对象先析构。
概念:
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。特征:1. 拷贝构造函数是构造函数的一个重载形式。2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。【注意】:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了。默认拷贝的浅拷贝会导致之后的两个对象指向同一块空间,如果这两个对象生命周期结束,调用析构函数,那么同一块空间就会被析构两次。【注意】:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。深浅拷贝到后面的章节会重点讲解。5. 拷贝构造函数典型调用场景:(1) 使用已存在对象创建新对象(2) 函数参数类型为类 类型对象(3) 函数返回值类型为类 类型对象class ClassName { public: // 如果是传值传参,形参是实参的临时拷贝,拷贝就会调用类的拷贝构造,就会造成无穷递归 ClassName(const ClassName& d) // 使用const为了防止d被修改 { _n = d._n; } private: int _n; };
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)注意:1. 不能通过连接其他符号来创建新的操作符:比如operator@。2. 重载操作符必须有一个类类型参数。3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。5. (.*) (::) (sizeof) (?:) (.) 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。// 全局的operator==,这样类中的成员变量就要公有,这样无法保证封装性 bool operator==(const ClassName& d1, const ClassName& d2) { return d1._n == d2._n; } class ClassName { public: // private: int _n; }; // 写成成员函数 class ClassName { public: // 判断相等==,对一个参数是左操作数,第二个是右操作数,成员函数有一个隐藏的this参数,所以只用传一个参数 bool operator==(const ClassName& d) { return _n == d._n; } private: int _n; };
// 频繁使用的<<流提取,也是也是在iostream里重载了<<运算符 // 但是i,d的类型不同,所以也构成函数重载 // 但是运算符重载和函数重载没有直接联系 int i = 0; double d = 0.0; cout << i; cout << d; class ClassName { public: private: int _n; }; // 想要重载这个运算符,这样写是不行的 // 这样写就是把这个函数变成ClassName的成员函数 // 成员函数调用的时候就会变成这样 d << cout; 这样是很奇怪的 // 函数第一个操作数是d,第二个才是out void operator<<(ostream* out) { out << _n; } // 所以可以写成一个全局函数 // 但是这样就不能访问类中的私有,所以使用一些方法使得在类外也可以使用 void operator<<(ostream& out, const ClassName& d) { out << d._n; } // 到这里还有一个问题就是它不可以像 cout << d1 << d2这样连续 // 这行的执行顺序是从左往右的,不同于赋值 // 这样就需要一个返回值 // 如果经常使用这个函数最好还是能以为内联inline // 定义为内联以后就要注意声明和定义不要分离,会导致链接错误 inline ostream& operator<<(ostream& out, const ClassName& d) { out << d._n; return out; } // 流提取>>也是差不多的 inline istream& operator>>(istream& in, ClassName& d) { in >> d._n; return in; }
1. 赋值运算符重载格式参数类型:const T&,传递引用可以提高传参效率返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this :要复合连续赋值的含义ClassName d1; ClassName d2(d1); // 这里是用d1去构造一个d2,所以调用的是拷贝构造函数 ClassName d3; d3 = d2; // 这里中间的=是赋值符号,所以调用的是赋值运算符的重载 class ClassName { public: // ClassName返回值可以支持连续赋值,如果不加&,在返回的时候就会调用一次拷贝构造 // 因为出了赋值运算符重载的作用域,this不会被销毁,所以可以使用&返回 ClassName& operator=(const ClassName& d) { if (this != &d) // 判断是不是给自己赋值 { _n = d._n; return *this; } } private: int _n; };
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数因为,赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。【注意】:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。一些类需要显示写拷贝构造和赋值运算符重载,比如:Stack……而有些类就不需要写。比如一些成员变量简单的类,默认生成就会完成浅拷贝;有些成员变量为自定义类型的类,默认就会调用它的自定义类型的拷贝和赋值。
// 特殊处理,使用重载区分,后置++重载增加一个int参数和前置进行区分 ClassName& operator++() // 前置:返回+1之后的结果 { _n += 1; return *this; } ClassName operator++(int) // 后置:返回+1之前的结果 { ClassName tmp(*this); _n += 1; return tmp; // 临时对象不能使用引用返回 }
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。class ClassName { public: // this指针的类型为ClassName* const this // const在*的后面修饰的是this这个指针,不可以修改this指针的指向 // 在函数后面加上const,this指针的类型就变成了const ClassName* const this // 这样this指针指向的内容也就不能修改了 void DisPlay() const { //…… } private: int _n; };
const对象只能调用const成员函数、不能调用非const成员函数,const调用非const代表权限的放大,从只读变成可读可写是不可以的。
非const对象可以调用const成员函数,非const调用const代表权限的缩小,从可读可写变成只读是可以的。
class ClassName { public: ClassName* operator&() { return this; } const ClassName* operator&() const { return this; } private: int _n; };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到指定的内容,显示的写出来后,把返回值设为nullptr,显示的写了,编译器就不会去调用默认生成的。
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。class ClassName { public: ClassName(int n) { _n = n; } private: int _n; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值,这样就有了初始化列表。
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。class ClassName { public: ClassName(int n) : _n(n) //, ... 其他成员变量 {} private: int _n; };
【注意】1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量,别名要定义就初始化
- const成员变量,定义就要初始化
- 自定义类型成员(且该类没有默认构造函数时)
- C++11打了一个补丁,在成员变量声明的地方可以给缺省值,这个缺省值就会给初始化列表
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。class ClassName { public: ClassName(int n) : _n(n) {} private: int _n; }; int main() { ClassName d1(0); // 直接调用构造函数 ClassName d2 = 0; // 这里是一个隐式类型的转换:构造 + 拷贝构造 + 优化 -> 直接调用构造函数 // 在这中间会产生一个临时变量 int i = 0; double d = i; // int赋值给double的时候就会产生一个临时变量 // double& d = i; 这样写就会报错,因为临时变量具有常性,const double& d = i; 这样写就可以 return 0; }
所以,用explicit修饰构造函数,将会禁止构造函数的隐式转换。
// 还有一个知识点就是匿名对象 // 生命周期只有这一行,即会调用构造函数,也会调用析构函数 // 有的时候匿名对象还是非常的好用 ClassName(0);
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。class ClassName { public: ClassName() {} // 想要访问静态成员变量可以写一个静态成员函数 // 它没有this指针,只能访问静态成员 static int Getn() { return _n; } private: static int _n; // 这里是声明,静态成员变量属于整个类,生命周期是全局的,存放在静态区 }; // 这里的_n就是一个静态成员变量,它不可以使用初始化列表初始化 // 也不可以给缺省值,缺省值也是给初始化列表的 // 所以它只能在类外面定义初始化 int ClassName::_n = 0
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区,属于整个类2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制// 有了静态成员就可以实现一个功能,例如要求一个类只能在栈上开辟空间 // 下面这种可以在栈上,可以在堆上,也可以在静态区开辟空间 class ClassName { public: ClassName() {} private: int _n; }; // 既然公开构造函数可以在任意区构造,所以就需要把构造函数私有 class ClassName { private: ClassName() {} private: int _n; }; // 但是这样就无法实例化一个类,构造函数只能在类中调用,那么就开放一个接口来返回一个创建好的对象 class ClassName { public: ClassName CreateObj() { ClassName cn; return cn; } private: ClassName() {} private: int _n; }; int main() { ClassName d = ClassName::CreateObj(); // 但是这样就无法调用这个成员函数 } // 这就需要把这个函数设为静态的 class ClassName { public: static ClassName CreateObj() // 设为静态的之后就可以调用了,它没有this指针,不需要使用对象去调用,对外是私有的,但是在类中就可以调用构造函数 { ClassName cn; return cn; } private: ClassName() {} private: int _n; };
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类
刚才重载operator<<的问题就可以使用友元解决,这个重载的问题就是没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。class ClassName { friend inline ostream& operator<<(ostream& out, const ClassName& d); friend inline istream& operator>>(istream& in, ClassName& d); public: private: int _n; }; inline ostream& operator<<(ostream& out, const ClassName& d) { out << d._n; return out; } inline istream& operator>>(istream& in, ClassName& d) { in >> d._n; return in; }
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数,没有this指针
- 友元函数不能用const修饰,函数后加const修饰的是this指针指向的对象
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递,如果C是B的友元,B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承的时候再详细介绍。
概念:
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:
- 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 1. 内部类可以定义在外部类的public、protected、private都是可以的。
- 2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- 3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A { public: // B定义在A里面 // B要受A的域限制,需要访问限定符 // B天生就是A的友元 class B { public: private: int _b; }; private: int _a; }; int main() { // A类的字节是4,是不包含B类的 A a; // 这是可以的 // B b; // 这是不可以的 // 需要这样才能创建对象 A::B b; }
写到这里类和对象的大部分知识都已经讲解完了,也是很久都没写博客了,还是要继续加油的,还要学习更多的知识。