一、概念
人们常说c++是c的超集,其中相较c扩展的一个重要理念就是面向对象概念。在c中,存在内置类型和自定义类型,自定义类型,则主要由结构体实现,它是一个包含各种内置类型或者自定义类型变量的一个变量类型,常见讲解对象就是图书目录,目录包含书的书名、作者名、出版社、出版日期、页数、价格和存放位置等,内部信息可以由char数组、int类型等内置类型实现,然后包含在book这一结构体中。
#define MAXLEN 40
#define MINLEN 20
struct Book{
char title[MAXLEN];
char author[MAXLEN];
float price;
...
};
如上就是c中一个Book结构体的声明,它的使用需要在前面加上struct声明这是个结构体,它定义的变量初始化也需要用{}来括起来进行初始化;而在c++中,定义结构体变量不需要用struct声明。上面的Book对象用类来说的话,就是只有数据成员,而且权限全部默认开放并不封装隐藏,而c++中默认数据成员是private,即私有的,并不对外显示,除此以外还有public公有和protected受保护的两种,这是类中数据成员最基本的权限类型,且其具有类内初始值的概念,就是类内数据成员可以有个默认初始值,在构造函数没有显式构造并初始化对象成员时,其默认值就是类内初始值。
除了类内数据成员,类还有方法成员,作为处理类对外的一个功能接口,所以一般编写类方法的时候,需要考虑其封装性和实现功能来命名,达到见名知义的效果。
二、重要的成员函数
类的成员函数有着静态成员函数、普通成员函数和特别的成员函数之分,普通成员函数和类的各个对象相关,调用时也是通过对象使用点运算符来进行函数调用,那调用时是这样,实现文件怎么写呢?
普通成员函数都内含一个this指针,隐式指向调用该函数的对象,我们的成员函数通过this这个额外隐式参数来访问调用对象。比如存在Book类的getPrice()成员函数和它的book对象,然后我们来调用一下:
book.getPrice();
这样调用时,编译器就会把book的地址传给getPrice的形参this,然后getPrice()可以直接调用book的数据成员price,而不用使用成员访问运算符了(成员访问运算符就是点运算符".“和箭头运算符”->"),我们可以把this看成常量指针,因为只要进行调用,它的指向就不会发生改变。
特殊的成员函数
类会定义关于对象初始化的方式,如果没有定义,编译器一般会给它进行一个默认的初始化方式,这里的初始化就依据其数据成员的默认初始值了,属于隐式的,而这些方式都由构造函数这一特殊的成员函数来进行界定。
构造函数是和类名同名的函数,首先介绍的是默认构造函数、c++11中,在不接受参数的构造函数声明后面加上=default即向编译器表明按照这个函数来生成默认构造函数,当=default出现在类内部,即函数声明处,即表明函数内联;如果出现在类外部,即函数定义处,函数默认不内联(这不废话吗?)。
普通的构造函数
除了给构造函数的参数列表设置默认值以外,也可以在声明数据成员的时候赋予初始值,像自定义类型的指针成员,一般都会赋予空指针的初始值;构造函数的目的是给数据成员赋初值,所以可以经常看到
Book(const std::string t, const std::string a, const float p):
title(t), author(a), price§{}
的构造函数出现,冒号和花括号隔起来的部分就是构造函数初始值列表,这是显式构造自己成员的一种方式,函数体为空因为数据成员都已经按照想要的方式构造了;除此以外还可以在类外定义构造函数体。需要注意的是,构造函数初始值列表只说明用于初始化成员的值,不限定初始化具体执行顺序。
隐式转换
c++中内置了关于内置类型自动转换的规则,因而对于类也存在隐式转换规则,当存在只接受一个实参的构造函数时,这个函数可以作为实参转换成这个类类型的隐式转换的函数使用,比如
Book(const std::string t):
title(t){}
然后剩下的这些个数据成员可以根据类内初始值或者默认初始值进行初始化,当在代码中存在需要Book类型,而其实际类型为std::string类型的时候,编译器可以自动执行一步转换,使其转换成Book类型的临时量供代码使用。而这种转换是危险的,因为隐式往往代表着不可见,所以常常隐藏着风险,对于不需要的隐式转换,我们可以在上面只有一个实参的构造函数前加上explicit声明,这样这个函数就只能用于显式构造。
拷贝构造函数和拷贝赋值运算符
拷贝构造函数和移动构造函数这两个函数定义了两个同类型对象,以其中一个对象去初始化另一个对象时干的事。
当我们进行直接初始化时,是要求编译器使用函数匹配来选择与提供的参数最匹配的构造函数,而使用拷贝初始化,则是右侧对象拷贝到左侧创建中对象中,有可能发生类型转换:
std::string s("miemiemie"); //直接初始化
std::string s1 = s; //拷贝初始化
std::string s2 = "mimimi"; //拷贝初始化
除了定义变量,在将对象作为实参传递给非引用类型形参、从返回类型为非引用类型的函数返回对象、用花括号列表初始化数组元素或聚合类中成员也会出现拷贝初始化。
拷贝构造函数的使用场景已经明了,什么样子的才是拷贝构造函数呢?
这种第一个参数是自身类类型的引用,且额外参数都有默认值的构造函数就是拷贝构造函数。当我们的这个构造函数是explicit的,它的拷贝初始化就是非法的,这个是它的限制。另外在拷贝初始化时,不一定使用拷贝构造函数,而是可以直接创建一个对象,即:
std::string n = "lllll";
等同于
std::string n("lllll");
当类类型没有定义拷贝构造函数时,编译器会自行定义一个合成拷贝构造函数,而合成的拷贝构造会根据成员类型来拷贝。前面说过使用explicit来阻止隐式转换,那拷贝行为呢?我们同样可以使用=delete来表示阻止拷贝:
struct Book{
Book(const Book&) = delete; //阻止拷贝
Book &operator=(const Book&) = delete; //阻止拷贝赋值
};
上面提到有拷贝赋值函数,这个函数就是接受这个类类型的赋值重载函数符,通常这种函数除了参数是所在类类型的引用以外,它的返回值也是该类型对象的引用。当这种函数没有被定义时,编译器会自动生成一个合成拷贝赋值运算符。
析构函数
上面说了构造函数,主要是进行数据成员的初始化工作,现在讲讲相反的。一个类类型对象的创建,需要对内部成员进行初始化才能使用,尤其是指针成员,更是需要进行空间的申请才能调用,那这个是使用前,使用后呢?当它失去了存在价值,生命域到了尽头呢?那就需要清除清理了,这里说的析构函数做的就是这个工作–善后,也就是前面申请了资源的,后面要逆序释放,前面创建了数据成员,后面就要销毁数据成员。
析构函数由波浪号和类名构成,不接受参数,没有返回值,一个类里有多个构造函数,但只有一个析构函数。它被调用的时候,就是对象离开作用域、该对象被显式销毁、作为临时对象被表达式使用且该表达式结束。和拷贝构造、默认构造、拷贝赋值一样,当类没有明确定义析构时,编译器会自行定义一个合成析构函数。析构函数自身不直接销毁成员,成员是在析构函数体之后的隐含析构阶段销毁,在这个对象的销毁阶段,析构函数体作为成员销毁步骤以外的部分进行。
上面都是关于一些非静态数据成员相关的一些不可或缺的成员函数,那静态成员呢?下面做一下回顾。
静态数据成员和静态成员函数
静态static,一般是用来描述变量作用域和生命域的,普通局部变量仅存在于它的块区域,比如函数中的变量,函数结束,变量销毁;但该变量以static修饰,则只有当程序销毁,该变量才销毁,否则它的值会一直存在于内存中的全局变量区,但不对其他同名变量产生影响;普通全局变量则是可被程序的其他源文件以extern外部链接,但添加了static以后,就只能存在于本文件,不能对其他文件产生影响。
而类的静态成员也类似,是属于类特有的特征,不独属于类的某一个对象。同样的,类的静态成员函数也不被类的对象特有,不包含this指针。类的静态成员的初始化通常在类外,由静态成员函数进行初始化,如果需要在类内进行初始化,则需要定义一个const类型,要求这个静态成员必须是字面值常量类型。
三、关于继承
在类中,有继承的概念使得相似类间形成一个层次结构。在继承关系中,被继承的类叫基类,继承了其他类的类型叫派生类,派生类还可以继续被继承,从而可以有派生类的派生类。在继承中,还有访问说明符对于继承的类型进行界定,从而形成更好的封装。
//基类
class Base {
int men;
public:
int f();
protected:
int fun();
};
//派生类
class Dervive : private/public/protected Base {
int men_der;
public:
int f_der();
protected:
int fun_der();
};
如上,这就是基类和派生类,基类可以访问公有成员和受保护成员,无论哪种继承方式,基类的直接派生类都只能这样。那定义时出现的访问说明符有什么作用呢?private等访问说明符已经对继承类型做出了限定,private继承,基类Base的私有成员以外成员成为派生类Derive的私有成员;public继承,派生类Derive继承基类Base的私有成员以外成员,作为自己的公有成员;protected继承,则是同时兼顾两者,当你只想与这个类的派生类分享基类成员而不是被公共访问时,可以使用这个继承。这么一来就比较明白了吧。派生类的派生类也不能访问它的父辈的私有成员,也就是说这个访问说明符的作用是用来界定派生类的派生类的访问行为的,同时也有使用派生类对象的代码的访问权限的。
当你定义了一个不需要继承的类,你可以在声明定义该类的时候再类名后添加final关键字。
class Last final{};
另外有一些人的习惯是继承的时候不显式指明继承类型,那这样就会默认private继承。
虚函数
在c++中,可以存在同名函数但这些个函数接受不同类型参数同时返回不同类型的结果的特性。比如相加函数add,我们可以定义一个只接受int类型的add函数,也可以定义一个只接受float类型的add函数,同时还可以定义接受两种类型的add函数:
int add(const int a, const int b);
float add(const float a, const float b);
int add(const int a, const float b);
...
这种定义不同功能的同名函数特性被称为函数重载,这也是实现c++中静态多态性的重要特性。然后可以知道的是,继承关系中,各种类都可以归结成一种类,那这一种类自然有一个同样的特性,比如:人有男人、女人,有孩子、年轻人、老人的区别,但他们都会思考,这个就是一个共有的一个特性thinking,但同样的事物带来的思考又是不一样的,这时候thinking就显现不同的结果,这就是多态。在类中,这个概念由虚函数来实现。
定义一个虚函数,需要在声明成员函数时前加上virtual声明,这样的成员函数被继承后,派生类可以根据需要对该函数进行重新定义,这就是重写override,这样一来,当该对象调用该同名函数,就会根据重写后函数的实现来显现效果。需要注意的是,每个基类都会有一个虚析构函数,无论该函数是否有可执行操作。
虽然派生类不一定需要重写覆盖它继承来的虚函数,但如果它准备这样,可以在派生类的该函数声明的尾端加上override关键字,这样如果没有重写实现,编译器就会报错,算是一种提醒。如果不重写,那这个派生类可以不声明该函数。
派生类和基类的类型转换
派生类继承了基类的成员,它的对象中包含了基类的相应部分,所以可以把派生类对象当成几类对象来使用,也能把基类指针、引用绑定到派生类对象的基类上。如下:
//定义基类对象和派生类对象
Base base;
Derive derive;
//定义基类指针ptr,把ptr指向派生类对象
Base *ptr = &base;
ptr = &derive;
//定义基类引用,绑定到派生类对象
Derive &ref = &derive;
派生类到基类的类型转换和其他转换一样,属于编译器进行的隐式转换,但不存在基类向派生类的转换,这种转换可以使得派生类对象或者对象的引用用在基类的引用上,反过来呢,派生类对象指针可以当做基类指针来用。
注:就算你的基类指针或者引用绑定了派生类对象,也不能执行基类向派生类的转换。
如:
//定义一个基类指针并指向派生类对象
Derive derive;
Base *ptr = &derive;
//定义一个派生类指针
Derive *ptr_der;
//把基类转成派生类
ptr_der = ptr;
虽然上面ptr指向派生类对象,ptr_der又是派生类类型指针,但上面把ptr_der = ptr的行为却是属于转换行为,是非法的。在派生类对象和基类对象间不存在类型转换,派生类向基类的自动转换只存在于指针和引用间。
虚函数的动态绑定
对于c++中的类,如果我们声明了一个虚函数,编译器会自动生成一张虚函数表,表中存储虚函数地址信息,并在类中添加一个指向虚函数表的指针vptr;但此时并不会初始化vptr,只有当对象构建,vptr才会初始化成虚函数表的地址。
当基类存在虚函数时,派生类可选择是否对继承来的虚函数进行重写,重写过的虚函数地址会取代基类的虚函数,由于基类的指针和引用可以指向基类对象或是派生类对象,通常我们只有在运行时才知道该基类指针或引用调用的虚函数是哪个版本。
在c++中,使用基类的引用或指针调用虚函数时就会发生动态绑定。
四、移动概念
更新中