1. 封装的主要目的是解决代码的维护性问题,经过封装的函数代码独立性高;
2. 封装的演变历史,以栈为例子介绍:
①成员(top、data[ ])都在main函数里,动作方法(push、pop)等也在main函数里(尼泊尔电线);
②成员(top、data[ ])在结构体体struct里面,动作方法(push、pop)等还在main函数里面(相当于未完成装修的房间,虽然电线整体布置,但是裸露的);
③成员(top、data[ ])和动作方法(push、pop)都在结构体(类)里面,动作方法(push、pop)对外只提供接口,达到既知其接口,又可以访问其数据;(C语言重在函数,c++重在类)
3. 构造函数的作用及特点
①无函数返回值,存在默认和自定义等多种情况(引导面试官);
②函数名与类名相同;
③自动调用:在通过类实例化对象时,构造函数会自动调用;
④可以重载;
4. 构造函数的种类及优缺点
(1)默认无参构造函数 VS 自定义无参构造函数
当类中无任何其他构造函数时,系统会默认生成;
若有其他构造函数,则该默认无参构造函数不会生成;其中自定义无参构造函数,可以在函数体里面初始化自定义初始化成员变量的值;注意若自定义的有参构造函数只有一个形参时,即只对一个成员变量进行初始化,即形成了类型转换构造函数,容易产生歧义,慎用!
下例中同样含有int类型(或char*)等类型与类类型(Student)之间转换的方法;
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
public:
//Student() = delete; //使用delete可以禁止系统自动生成默认构造函数;
Student() = default; //使用default可以显式的让系统自动生成默认构造函数;
Student(int num, char *name, int age)
{
m_num = num;
m_age = age;
int len = strlen(name)+1;
m_name = new char[len];
strcpy(m_name,name);
}
explicit Student(int num) //该构造函数只有一个参数,称为类型转换构造函数;int型→类类型Student;本例中前面加了关键字explicit,禁止系统隐式转换,所以s3无效,会报错;
{
m_num = num;
m_age = 0;
m_name = nullptr;
}
operator int() //重载了int类型,即可以将类类型转换为int类型;
{
return m_num;
}
private:
int m_num;
char * m_name;
int age;
};
int main(int argc, char **argv)
{
Student s1; //会报错,因为此时已经有自定义有参构造函数,默认无参构造函数失效,系统不生成;
Student s2(1,"zhangsan",12);
Student s3 = 1; //调用类型转换构造函数,慎用!容易产生歧义;这里系统内部发生隐式转换,将int型转换为Student(类类型);应习惯使用s4样例;因为有explicit,这里会报错;
Student s4(1);
int num1 = static_cast<int>(s4); //利用重载operator int()将类类型装换为int类型;
int num2 = s4.operator int(); //方式二;
return 0;
}
(2)默认拷贝构造函数 VS 自定义拷贝构造函数
利用已有对象初始化一个新的对象,两个成员之间的变量对等赋值,默认情况下是浅拷贝,但是存在“多次释放同一个空间的问题,即free() : double free”;
自定义拷贝构造函数是深拷贝,参与拷贝的两个对象各自的指针成员变量指向自己独立的堆空间;
引申1:面试题:解释浅拷贝与深拷贝?
①类对象拷贝是指利用已有对象初始化一个新的对象,若无自己定义,则默认浅拷贝,系统默认的拷贝构造函数和等号运算符重载(operator=)都是按照浅拷贝的方式,即两个成员之间的变量对等赋值(包含指针变量),但是当对象内有指针成员时,会导致出现“多次释放同一个空间的问题,即free() : double free”的段错误;
②深拷贝就是自己定义,新的类对象中的指针成员是新生成的独立的空间(new),其大小等同于待拷贝的对象指针成员所指向的空间大小,其他变量则是对等赋值,解决了浅拷贝中的段错误;并且可以利用下面的移动对象构造函数进行优化;
引申2:面试题:拷贝函数的调用时机?
①用已有的对象初始化新的对象;
②对象作为形参时;
③对象作为函数返回值时;
(3)赋值运算符重载函数
默认运算符重载函数是浅拷贝,当优化为深拷贝后,其主要不足是当大量的接收函数创建的临时类对象时,需要不断地生成临时对象→拷贝对象→释放临时对象;整个过程耗时较长,且降低了程序性能;
优化思路为直接将新建的临时对象的空间交给左值,不用经历“新建→拷贝→释放”这样的耗时过程了,就形成了移动拷贝构造函数;
(4)移动拷贝(对象)构造函数
内部机制是将临时对象的指针成员空间赋给新对象,然后将临时对象的指针指向置为空(原对象就不可以再使用),其他值对等赋值,有移动拷贝构造函数和已移动等号运算符重载两种实现方式;在两种情况下系统自动调用移动对象函数:
①新对象先定义后接收函数的返回值(临时对象);
②利用std::move()进行强制转换(告诉编译器括号内的是右值,匹配移动对象构造函数);
通常在编译时加上-fno-elide-constructors的选项,作用是关闭函数返回值优化,同时减少拷贝构造函数的调用,转换为调用对象移动函数,提高程序运行性能;
Test (Yest &&other) //方式一:移动拷贝构造函数
{
cout<<"move Test copy"<<endl;
this->n_name = other.m_name;
other.m_name = nullptr
}
Test & operator = (Test &&other) //方式二:移动等号运算符重载
{
cout<<"move operator = "<<endl;
this->m_name = other.m_name;
other.m_name = nummptr;
}
(5)委托构造函数
①该类型主要是为了减少多种构造函数形成的代码冗余,内部机制是调用已有的构造函数,初始化新的构造函数;
②执行的顺序是委托构造函数会将控制权交给代理构造函数,在代理构造函数执行完之后,在执行委托构造函数的主体;
③实现思路:先写全参的构造函数(称为代理函数),后写委托构造函数;
④缺陷:如果一个构造函数为委托构造函数,那么其初始化列表就不能对成员和基类进行初始化,只能使用调用委托构造函数时带来的参数值去匹配代理构造函数;
几个例子:
5. 左值、右值、左引用、右引用
(1)左值的特点:
①指向特点内存的具有名称的值(具有对象名或者变量名);
②有一个相对稳定的内存地址;
③有一段较长的生命周期,长期存在的值;
(2)右值的特点:
①不指向稳定内存地址的匿名值(例如常数1,3,5...)(怎样理解?);
②生命周期很短,通常是暂时的;
(3)区分左右值,可以通过&运算符获取地址的就是左值,否则就是右值;
(4)左值转右值,以下两种均可以将对象转换为右值,进而匹配类对象中的移动对象构造函数(避免了拷贝函数的费时低性能):
①static_cast<type&&>(对象);
②std::move(对象);
(5)引用
①int num = 5;
②int &l_num = num; //l_num是左值引用,绑定左值;
③const int &c_num = 5; //c_num是const左值引用,const左值引用即可以绑定左值(常量),也可以绑定右值;
④int &&k_num = 5; //k_num是右值引用,注意是两个&&,右引用绑定右值;
6. 初始化列表
初始化列表含有双重含义(定义+初始化)主要是解决那些定义就必须要初始化的变量,有以下几种:
①const修饰的成员变量;例如:const int index;
②验收引用的成员变量;例如:int &l_num;
③成员对象(另一个类的对象),即类对象里面含有另一个类的对象;
7. 类的平凡属性
(1)当类对象中含有①自定义无参构造函数、②自定义拷贝构造函数、③赋值运算符重载函数、④移动拷贝构造函数、⑤类型转换构造函数、⑥委托构造函数时,就是非平凡类;否则,不带有以上函数时,就是平凡类;
即除了默认的构造函数(无参构造函数、默认拷贝构造函数),外都会破坏函数的平凡属性;
(2)(带以上函数)非平凡类:需频繁进行赋值等操作,运行耗时较长;
(3)(不带)平凡类:内存对内存,运行效率高,因为不用类对象复制、转移等操作;
(4)采用default关键字,可以采用系统默认生成的构造函数,可以尽量保持类的平凡属性,从而提高程序的运行性能;是良好的代码习惯;示例如下:
Struct Node
{
Node() = default;
int num;
};
8. 对类中的成员进行操作
(1)static修饰(成员、函数)
①保持在C语言中的相关作用(修饰局部变量时,改变了局部变量的生存周期,由函数生存周期增加为程序生存周期,作用域未改变,还是函数内;修饰函数或者全局变量时,改变了函数或者全局变量的作用域,仅限该声明的.c文件内使用,在其他文件无法通过extern使用);
②static修饰类对象中的成员变量时,该成员变量成为类的静态变量(也称作类成员),可以直接通过类名访问而不用通过实例化对象,该静态变量被该类所有的对象共享,适用于对象之间的通信;且修饰的成员变量需要在类外初始化(需要加上类名::格式);
此时这个成员不是该对象的成员,而是称作类成员(被类型所共有);
#include <iostream>
using namespace std;
class A
{
public:
void set_num(int num)
{
m_num = num;
}
int get_num()
{
return n_num;
}
static void func() //静态成员函数,也称类的成员函数;
{
cout<<m_index<<endl; //不能访问非静态成员,只可以访问静态成员
}
//private: //若想利用类成员的共享特点,需要将该成员属性放到public,否则也会受private属性限制;
int m_num;
static int m_index;
}
int A::m_index = 7; //用static修饰的成员属性,一定要在类外初始化;
int main(int argc, char **argv)
{
A a;
a.m_index = 1; //访问类成员方式一:对象名.类成员名
A a2;
a2.m_index = 2;
cout<<A::m_index<<endl; //访问类成员方式二:类名::类成员名
//输出2
return 0;
}
③static修饰成员函数时,该成员函数称为类的静态成员函数(也称作类的成员函数),可以直接通过类名访问而不用通过实例化对象,静态成员函数没有默认的this指针指针生成,所以该类函数不能访问非静态成员(即没有被static修饰的成员),主要用于与C语言编程中用于混合编程用于回调函数;
④static同样受限于private,所以static只有放到public区域才可以发挥作用;
⑤工程应用:主要有两点,一是修饰的静态成员变量可以用于该类对象之间的通信(因为可以共享);二是应用于C与c++混合接口编程;
(2)const修饰,const将修饰的变量转换为常量,既可以修饰函数,也可以修饰成员变量,
①修饰函数时,该函数只可以访问成员(输出),不可以改变成员的值,但是可以在对应成员前面加上mutable,那么该成员即可被该函数修改;
②修饰对象时,该对象只可以调用const修饰的函数;
(3)mutable修饰成员
①可以在被const修饰的函数中被修改;
9. noexept、explicit、default、delete、mutable关键字在类对象函数中的作用?
(1)noexept修饰的函数,表明程序员向编译器保证该函数不会发射异常,那么编译器也就没有必要为处理这个“可能”发生的异常添加一些事先预备好的目标代码,这在一定程度上减少了函数编译后生成的目标代码;c++11为所有类的析构函数都加上了“隐式”noexcept声明;
使用的时机一般:当设计移动函数时,为其加上noexcept,以便此类的对象在使用时可以用移动操作来代替拷贝,提高了程序运行性能(因为有些标准库容器除非知道移动操作是无异常的,否则就会进行拷贝)。
(2)explicit是禁止隐式转换的;例如当使用功能类型转换构造函数时,加上explicit则无法进行隐式转化,类型转换构造函数即失效(因为其内部进行了隐式转换);
(3)default关键字通常修饰类对象中无参的构造函数,尽量保持类的平凡属性(单纯的结构体),对象之间赋值通过内存之间赋值操作,从而提高代码的运行性能;
(4)delete关键字用以修饰类对象中的函数,禁止修饰的函数生成,表明在该类型对象中没有该函数,根据其位置分为两种情况:
①放在public时,该修饰的函数在编译时期检查与优化(节省时间,相当于错误提前出现,节省编译时间);
②放在private时,该修饰的函数在链接时检查优化(在编译期后,错误检查的晚,容易导致浪费编译时间);
(5)mutable修饰的成员可以在被const修饰的函数中被修改,通常在类对象含有const修饰的函数时使用;
10. 封装为什么能够提高代码的维护性?
①封装的本质是将数据和函数(动作)绑定在同一个类对象里面,后续可以通过对象来完成操作;
②封装后的函数对外简单的接口,操作方便;
③封装的对象内部含有权限属性设置,函数与数据相对独立于安全;
④封装后的对象里面含有大量的构造函数、析构函数,极大地方便了开发者的对象定义与操作;