提纲
C与C++的不同
指针与引用
申明与定义
C++构造函数种类
构造函数的使用场景
初始化序列
1.C与C++的关系
C++语法构成:
l 继承C的语言的部分:代码块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(build-in types)、数组(array)、指针(pointers)、宏(macro)
l 面向对象部分:类(class、构造和析构)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、虚函数(virtual function)动态绑定
class是一种(自定义的)数据类型,这点与int,char等在本质上没有什么不同(对象生成、使用、销毁等,内置类型的变量,也可以认为是一种对象,这点在其他面向对象语言体现得更彻底),class最大的优势在于用户可以通过自定义扩展其功能,从而为程序设计提供很多内置类型所不能提供的能力,从而更好的完成程序设计(职责与能力成正比)。类型和对象的关系,类似于工厂之中摸具和产品,使用同一种摸具可以生产中无数同样规格的产品。
用户自定义类型这点是与C的struct类似,与struct不同点主要在于,class可以定义成员函数(一般的成员函数与构造、析构这种特殊的成员函数),而struct只有成员变量(struct可以使用函数指针“变相”实现成员函数),class具有封装级别,而struct没有(或者只有public)
C++的面向对象部分,最重要的三点:封装、继承、多态
l Template C++:C++的范型编程(genericprogramming)
l STL(standardtemplate library):包含容器(containers)、迭代器(iterators)、算法(algorithms)和函数对象(function object)
从实用的角度出发,学习的顺序应为:C的部分->面向对象部分->STL->Template C++
本课程重点也是在于C++的面向对象的特性部分,所有的讲解基本上都是围绕着该部分进行,(STL本身就是Template C++的一个完美体现)
2.几个重要的术语:
声明:通过固定的格式,告诉编译器某个东西的名称和类型(告诉编译器有这么一个东西,但并不说明其细节,针对class与function),同时不分配内存空间(针对object,判断语句是声明还是定义,最重要的方法就是判断是否分配了内存)。
定义:告诉编译器其细节(针对class与function),或者分配内存空间(针对object)
类、对象的声明与定义
类的声明:
class A;
一般用于.h的前置声明(forward declaration),某个类在定义时,使用到另外一个类时,可以不引入其定义所在的头文件,而使用前置声明,从而减少头文件之间的包含关系。但是只能定义了该类的指针和引用、形参、返回值。如果是继承、成员变量、以及内联函数中使用到该类型,则不能使用前置声明(出现incomplete type编译错误)。
类的定义:
说明类的数据成员和函数成员(见上图),以及数据成员所需要的内存大小。一般放置在.h文件被其他文件引用,目的是为了实现类的“一处定义,多处使用”。需要注意的是,类的定义一般放在.h文件,而类的成员函数的定义,一般是放在.cpp文件。如果将成员函数的定义放在类定义中,则编译器就会将该函数自动设置成内联类型(内联函数没有函数开销,属于用空间换性能的优化方法)。
类中使用到自身的类型的成员变量
如果类定义中,使用到另外类的对象(如上图中calss A中存在一个class B的对象),则class B的定义需要出现在class A之前(同一个头文件,需要在其之前定义,不同头文件,需要引入该头文件)
但是类定义中,是无法将自身作为其数据成员的(因为在类定义过程中,类的定义还是不完全的)。变通的办法,使用类自身对象的引用,指针来代替直接使用对象。
对象的定义:
一般出现在.cpp文件中,使用类似于以下形式出现
A a;
class A a;
对象的定义,完成两个工作,为对象分配存储空间(依据定义的方式不同,使用的内存不同)、调用类的“某个”构造函数完成对象的初始化。
对象的声明:
同C中全局变量的声明,一般用于全局对象,不太常用。
指针和引用
int i = 100;
int& refI =i;
引用是C++中的一种新的复合类型(compoundtype,指的是使用已有数据类型和“&”组合来定义),引用是被引用对象的一个“别名”,其在本质上和使用上,均和指针类似(可以将引用理解为一个const pointer)。其与指针的差异,主要体现在:
指针在定义时,可以为空,而引用不行,必须在定义时给定(这个会体现在对象的构造函数时,引用成员必须使用初始化序列)
指针定义后,其值可以更改,也就是把同一种类型的不同地址赋给同一个指针,而引用不行,引用可以作为右值赋给其他引用,但是引用本身“指向”的对象不能变更
在代码中,指针使用前,必须判空,但是引用不需要(该特性可以节省大量的指针判空操作)
Const限定符:
const限定符的本意在于加了const之后,被限定的对象就不能被修改,其理解的难点在于const限定符在C++中可以与各种语法要素进行结合,并且还有多种结合方式,在此只作一些列举,深入理解主要还是靠多使用,多练习。
使用const限定符定义的变量,必须在定义的同时完成初始化(该特性影响到类的const成员的初始化必须放在初始化列表)
将const变量赋值给非const变量是允许的,反之是不允许的。
限定变量
const intbufsize = 512; //之后对于bufsize的重新赋值,均会编译错误
限定指针
int b = 100;
const int* a =&b;
int const *a =&b; // 上述两种情况,const在“*”左边,均用于限定指针所指向的内容为const,也即b不能修改,而a可以修改
int * const a =&b; // const 在“*”,限定指针本身不能修改,也即a的值不能修改
const int* consta = &b; // const在“*”左右两边均有,指针指向的对象与指针本身均不能修改,也即 a和b的值均不能修改
限定引用
const int i = 10;
const int&refI = i; //因为i本身被const限定,所以对于i的引用refI也必须为const,这样可以防止对于refI的修改(如 refI = 20),从而修改i
限定函数参数
void fun(const int i);//参数i在函数中不能修改
限定函数返回值
const int fun();// 函数的返回值在函数调用之后,不能修改
限定类的函数成员
class A
{
public:
void fun() const; // 函数内部不能修改class的成员变量i,j
private:
int i;
int j;
};
重载与重写(overload & overwrite)
重载
函数名相同而形参表不同的两个函数
void fun();
void fun(int);
void fun(int,char);
重写(重写)
子类对于父类中某个函数的重新定义,他们之间同名且函数签名(返回值+形参表)相同,为同一个函数的不同实现版本。
3.如何生成一个对象
构造函数
一种特殊的成员函数,用来创建对象(变量定义),可以被显式调用或编译器自动调用,构造函数的工作是保证每个对象的数据成员都具合适的初始值。
构造函数与普通成员函数的区别在于:函数名固定(与类名相同)、没有返回值
构造函数的目的就是完成类的数据成员的初始化(也即上例中的i如何获得初值)
构造函数依据其是否使用已有对象来初始化成员,分为两种,如果是用已有的对象来初始化,则为copy构造函数,如果不是,则为普通的构造函数,普通的构造函数,如果可以不带实参使用(本身不带形参表,以及有形参,但是形参是默认参数形式),则为默认构造函数。
特别需要注意的是,第四种方式,虽然使用了“=”,但并不是赋值操作,而是一个调用copy构造函数的过程(有兴趣的同事可以重载“=”操作符进行验证)。
构造函数除了上述之外,还在很多地方被使用到,比如函数调用的时候入参、返回值的传递。
4.如何销毁一个对象
析构函数
对象被构造函数创建出来之后,需要使用析构函数进行销毁,与构造函数类似,析构函数也是一种特殊的,没有返回值的,与类名相同的函数,与构造函数不同的是,其函数名前面加“~”,同时没有形参列表。
析构函数,也是与构造函数一样,分为编译器隐式调用和代码显式调用,调用的类型和调用的时间点,具体取决于对象生成的方式(使用new操作符生成的,均要显示的使用delete操作符调用析构函数)以及对象的生命周期(生命周期与对象所处的作用域相关)。
5. 空类
编译器会为每个类默认实现一个默认构造函数,copy构造函数(浅拷贝版本,如果有指针和引用,要格外小心),一个析构函数,一个“=”操作符、以及“&”、“->”(不可被重载),如果自己定义的class没有重载上述函数(操作符也是函数),则当需要使用到这些函数时,编译器自动调用默认版本,如果用户已经提供了这些函数,则编译器就不会提供相应的版本(构造函数比较特殊,只要提供了一个构造函数,则默认版本的构造函数均不提供)
另外,如果一个空类的对象,它的大小是多少呢?也即sizeof(a1),会是多少呢?
6. 构造函数的初始化序列
类的构造函数的一个主要任务是对类的成员变量进行初始化,那么,类的成员变量是如何被初始化的?
但实际上,类的成员变量的初始化,在上述构造函数的初始化之前,编译器已经将成员变量使用初始化序列初始化过一次,这个过程会是在构造函数初始之前。
从下面的例子中可以看到完整的类的成员变量的初始化过程,在通过调用A5的构造函数生成对象a的过程中,首先在调用构造函数时,实参b到构造函数形参,有一次调用copy构造函数的过程,第二步,编译器调用成员变量b的默认构造函数,第三步才是形参对成员变量b的赋值过程。从上面的步骤来看,其实在构造函数内部完成并不是成员变量的初始化过程,而是成员变量的赋值过程,成员变量的真正的初始化是在构造函数函数体执行之前就已经完成了。
如果类的成员变量有引用,const类型,以及父类不存在默认构造函数,均需要显式的通过初始化列表来进行初始化。
从之前的例子中已经看到,类的成员变量的初始化,是在初始化序列中完成的,但是其顺序,并不是按照成员变量在初始化列表中顺序进行初始化的,而是在类定义的顺序执行初始化的。
7. 练习
1. 编写一个名为Person的类,表示人的名字和地址,使用string来保存每个元素。分别为Person编写默认构造函数、只有人名(地址使用默认值)、只有地址(人名使用默认值)、以及既有人名和地址作为参数的构造函数版本、copy构造函数,并分别用这些构造函数生成一个对象。
2. 为Person类提供获取人名和地址的方法(get方法),将结果通过cout打印出来
3. 为成员变量和成员方法增加封装级别,哪些应为public,哪些应为private,解释你的依据(写在注释中)
4. 尝试为Person类增加const属性,不仅是成员变量,还有成员方法,解释你的依据(写在注释中)
5. 定义两个类X和Y,X中存在一个指向Y的指针,Y的引用,Y中有一个X类型的对象。使用他们的构造函数生成对象