C++面向对象程序设计期末复习笔记[吉林大学](结合历年题速成85)

1.头文件

头文件的作用就是被其他的.cpp包含进去的.它们本身并不参与编译,但实际上,它们的内容却在多个.cpp文件中得到了编译.根据"定义只能一次"原则我们知道,头文件中不能放定义.(注:int a;是定义,开辟存储空间了,而 extern int a;是一个声明,因为没有开辟空间)

但是,"不能放定义"这个规则有三个例外:

1)可放const对象的定义.因为全局的const对象链接属性默认是内部链接,所以只在当前文件有效.所以即使头文件被包含到多个.cpp文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义.同理,static对象的定义也可以放进头文件.

2)可放内联(inline)函数的定义.遇到inline函数时,编译器就需要把它内联展开(根据它的定义),而并非普通函数那样可先声明再链接(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行.inline函数若只能定义一次可就太麻烦了,所以C++规定内联函数可以定义多次,只要其在一个.cpp文件中只出现一次且在所有.cpp文件中,这个内联函数的定义是一样的,就能通过编译.故把内联函数定义放进头文件,是个明智的做法.

3)可放类定义.创建类的对象时,编译器需要清楚这个类的定义,才能分配空间,所以应该把类定义放到头文件中.在C++中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的.因此把函数成员的定义写进类定义体,一起放进头文件中是合法的.(注:但是如果把函数成员的定义写在类定义的头文件中但没有写进类定义,是不合法的,因为这个函数成员此时就不是内联的了;一旦头文件被两个或两个以上的.cpp文件包含,这个函数成员就被重定义了.)

综上,头文件的规范
1)一般包括类定义,函数声明,extern变量(注:定义于函数外的变量称作外部变量, 函数内的变量称为局部变量;若在函数内部定义了与外部变量名称相同的变量, 则不会使用外部变量)
2)在编译时已知的const对象定义(用常量表达式初始化)
3)inline函数的定义
4)类定义中,能用指针或引用的就别用对象进行声明,因为引用和指针只需一个类型声明,而对象需要该类型的定义,此时要为声明式和定义式提供不同的头文件.
5)头文件中不能用using:
e.g.只能用std::cin>>a;而不能写using std cin; cin>>a;
头文件卫士
b.h中include了a.h,而且一个程序员在不知情的情况下在一个.cpp文件中同时include了a.hb.h,这就在同一个文件中include了a.h两次,虽然以上写的三个例外在多个源文件中可以被定义,但却不可以在同一个源文件中定义两次,这就出现问题了.为了解决这种问题,需要用头文件卫士#ifndef,#define....#endif

2.变量声明(特总结extern)

变量声明相当于向编译器保证此变量以给定的名称和类型存在,使得编译器不需要知道变量完整细节即可继续编译,可以在程序中多次声明一个变量,但是一个变量只能在某个函数,代码块或文件中被定义一次.
当你在a.cpp中声明了一个extern变量var后,你可以在a.cpp中用这个变量,声明语句告诉了编译器:这个符号是在其他文件中定义的,我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧.但注意:extern变量必须是能够引用到的,也即被引用的变量var是外链接的,C++中能够被其他模块以extern修饰符引用到的变量通常是全局变量
注:你不能写extern int i=3;这种语句,会被编译器提示不能对外部变量的局部声明进行初始化;但是你可以分两步:

	extern int i;
	i=3;//这种写法就是对的了

extern 修饰函数声明

如果a.cpp里有一个函数int f(int x)那么可以在b.cppextern int f(int x)
那么这样直接引用和用头文件引用函数有何区别?这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间.

3.构造函数与析构函数


实现自定义构造函数必须使用初始化列表的情况:

1)类中有const数据成员,e.g.const int num;(注:const对象或引用只能初始化但是不能赋值,构造函数的函数体内只能赋值而不能初始化,因此初始化const对象或引用的唯一机会是构造函数函数体之前的初始化列表中.)(换种说法:常量只能初始化不能赋值,所以必须放在初始化列表里面)
2)类中有引用型数据成员,e.g.int&a;引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
3)类中有对象成员但其类型中没有无参构造函数,所以必须在初始化列表中指明相应有参构造函数.
4)基类中缺少无参构造函数,必须在初始化列表中指明基类的有参构造函数.
5)没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化


派生类构造函数的初始化列表问题:

派生类构造函数的初始化列表可以包含:

  1. 基类的无参构造函数
  2. 基类的拷贝构造函数
  3. 派生类中非静态对象成员的初始化
  4. 派生类的子对象初始化(//q:跟3是不是一码事?)
  5. 派生类中一般数据成员的初始化

不能包含:

  1. 基类中非静态对象成员的初始化
  2. 基类成员变量(例如:int base_a;的初始化,如果非要初始化基类中的成员,只能调用基类的有参构造函数进行初始化,如下:)
 	B():A(5){}//基类A的有参构造函数为A(int i=0):base_a(i){}

(或者如下:)

	B(){a=5;}//不在初始化列表中直接初始化的基类的数据成员,这样是可以的
	//但这种方法的原理不再是初始化,而是改变a原本的值!!
	//当然,你应该尽量使用初始化列表而非赋值.
	//注:B不能访问A的私有成员 只可访问保护成员!


创建派生类对象时,构造函数的执行顺序

1.基类的构造函数
若有初始化列表中的显式调用基类的有参构造函数,就调用
若无,则编译器把基类的默认构造函数插入到初始化列表中,即B() => B():A()
2.对象成员的构造函数
若在初始化列表中则直接调用拷贝构造函数

3.派生类的构造函数


调用拷贝构造函数的情况:

1)用类的一个对象去初始化另一个对象时
2)函数的形参是类的对象,调用函数,进行形参与实参的结合
3)函数的返回值是类的对象,函数完成调用返回时(产生一个临时对象


析构函数私有化(或者protected化)
析构函数私有化可以保证一个对象只在堆上生成,只能用new生成.而且不能delete,因为delete会调用析构函数.想要把它释放,需要弄一个成员函数完成delete操作.如下:

void OnlyCanBeNew::Destroy() 
{ 
    delete this; 
} 

(注:既然只允许delete对象,那么该对象一定是建立在堆上的,因此应禁止用户在函数外直接调用构造函数在栈上,因此构造函数也应是private的)

○还有一种情况,当你想在析构之前再完成一些事情的时候,可以先把直接的析构函数给private化,再写一个函数先去完成你想让这个函数在析构函数之前做的事情,再在函数最后执行析构函数,如下:

class A
{
public:
   A(){}
   void ReleaseObject()
   {
      //可以在这之前做一些其他事情
      ...
      ~A();//最后析构
   }
private:
   ~A(){}
};

这样就没有办法delete了,因为~A()无法被别人调用。但是别人可以通过调用
public的函数ReleaseObject,这个函数本身是有权利调用~A()的。这样就确
保了在析构之前一定会做一些你想做的事情。这也可以说~A()ReleaseObject函数包装起来了.

4.各种关系(组合,聚合等)


组合与聚合
(组合是聚合的一种形式)组合与聚合的有本质上的区别:

○若A与B是组合关系,则B在A创建的时刻创建,A包含有B的全局对象(???没看懂这句话)

组合方式一般代码会这样写:
A类的构造函数中创建B类的对象,即当A类的一个对象产生时,B类的对象随之产生,当A类的这个对象消亡时,它所包含的B类的对象也随之消亡.

○若A与B是聚合关系,则A可以包含B对象,但B不是A的组成成分,A包含有B的全局对象,但B可以不在A创建的同时创建.

聚合方式一般代码会这样写:
A类的对象在创建时不会立即创建B类的对象,而是等待一个外界的对象传给它

e.g.在A类中引用B类的一个引用b,当A类消亡时,b这个引用所指对象也
同时消亡
(没有任何一个引用指向它,成了垃圾对象),这种情况叫组合;
反之b所指的对象还会有另外的引用它,这种情况叫聚合


依赖关系,泛化关系

○依赖关系
如果在类中用到对方(如:作为类的成员;作为函数返回值;作为函数接受的参数;在函数中被使用到),少了对方连编译都无法通过,则称为依赖关系.

○泛化关系
泛化关系其实就是继承关系,是依赖关系的特例.(若B类继承了A类,称AB间存在泛化关系)

注:依赖关系耦合最弱!!

5.虚函数,虚基类(各种虚)

虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。


虚函数特点以及一些注意事项:
1)可以寻址;这一点就排除了构造函数和内联函数.(注:虚函数可以是内联函数,但是在虚函数表现多态性的时候不能inline.因为虚函数是在运行期表现多态性,而inline是在编译期让编译器来判断能否inline)因为虚函数表中存放的是虚函数入口地址,如果函数不能寻址,自然不能是虚函数.
2)依赖于对象调用.因为虚函数存在于虚函数表中,有一个虚函数指针指向这个表,所以要调用虚函数,必须通过虚函数指针,而虚函数指针是存在于对象中的.
3)动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式:
  指向基类的指针变量名->虚函数名(实参表)
  或 基类对象的引用名. 虚函数名(实参表)
4)类的成员函数中静态成员函数也不能是虚函数,因为静态成员函数不属于任何一个对象或实例,也就不存在this指针的使用,而虚函数表恰恰要用this指针(this指针->vptr(4字节)->vtable ->virtual虚函数)
5)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”.
6)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数


虚基类:
“B和C同时继承A,而B和C都被D继承”这种菱形的继承结构,需要用到虚基类.
其实这种情况下,除了虚基类之外,还可以用作用域标识符来区分B和C中的func(),如B::func().

#include<iostream>
     using namespace std;
     class A{
     	public:
     		A(){a=5;cout<<"A="<<a<<endl;}
     	procted:
     		int a;
     	};
    class B:public A{
    	public:
    		B()
    		{a=a+10;cout<<"B="<<a<<endl;}
    };
    class C:public A{
    	public:
    		C(){a=a+20;cout<<"C="<<a<<endl;}
    };
    class D:public B, public C{
    	public:
    		D(){
    		cout<<"B::a="<<B::a<<endl;
    		cout<<"C::a="<<C::a<<endl;
    		}
    };	
 int  main(){
  D obj;
  return 0;
  }

以上程序的构造函数调用顺序:A() B() A() C();因为先定义B,而B又是派生自A,所以先调用A的构造函数,再B的构造函数,又定义了C,同理.

但是,我们来看如果定义为虚基类(也就是在继承的那个冒号和"public A"之间再加一个"virtual"即"virtual public A")(其实写成"public virtual A"也是一样的),此时ABC三个类里的数据成员a不再是无关联的,BC中的a不再是A中拷贝过去的a,相反的,现在这个a由基类A,两个派生类B,C所共有,BC构造函数对同一个a进行修改.如下:

#include<iostream>
using namespace std;
	class A{
	public:
		A(){
			a=5;cout<<"base="<<a<<endl;}

	protected:
	 int a;
};

class B:virtual public A{
	public:
		B(){a+=10;cout<<"base1="<<a<<endl;}
};

class C:virtual public A{
	public:
	 C(){a+=20;cout<<"base2="<<a<<endl;}
};
class D:public B,public C{
	public:
		D(){cout<<"derived a ="<<a<<endl; }
};
int main(){
D obj;
return 0;
}

我们此时再看他们的构造函数执行顺序:
首先B和C都是由A派生出来的,所以先统一构造A,调用A的构造函数;然后,按照顺序调用B和C的构造函数,总体顺序是这样:A() B() C();

*同一层级的继承中,基类的顺序:虚基类优先于非虚基类构造函数的执行
*如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用,如果未被列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类对象.所以,要求这个虚基类必须要有无参构造函数,没有的话会报错.

注:
1.基类的析构函数必须定义为virtual,如果不是虚函数的话,子类的析构函数将不被调用,就没法正确释放子类的空间,造成内存泄漏.(见下一个主题内存泄漏)


纯虚函数和抽象类

纯虚函数:就是没有函数体而直接在括号后边写"=0"的函数:e.g.virtual double func()=0;
意义:如果是一个纯虚函数,则在虚函数表中,其函数指针的值就是0.即如果是纯虚函数,那么就在虚函数表中实实在在的写上0,如果是普通的虚函数,那就肯定是一个有意义的值(入口地址).
我们把含有纯虚函数的类称作抽象类.当基类是一个抽象类时,子类也可以是抽象类,当然也可以不是抽象类.例如基类base里面有俩纯虚函数virtual int f1()=0,virtual double f2()=0,在子类child1里面,我们把f1"非纯虚函数化"也就是用函数体代替掉"=0",例如可以virtual int f1(){printf("000");}而保持virtual double f2()=0,这样的话child1仍然是个抽象类,无法实例化;然后再让子类child2去继承child1,并且把f2也变成非纯虚函数,这时child2就不再是抽象类,而可以实例化了.

6.内存泄漏

造成内存泄漏的可能原因:

1)当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄漏
2)delete掉一个void*类型的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄漏,如下:

int main() {
	Object* a = new Object(10, 'A');//Object*指针指向一个Object对象;	
	void* b = new Object(20, 'B');//void*指针指向一个Object对象;
	delete a;//执行delete,编译器自动调用析构函数;
	delete b;//执行delete,但编译器不调用析构函数,导致data的内存没回收;
	return 0;
}

3)new创建了一对象数组,内存回收时却只调用了delete,没用delete [],导致只有对象数组的第一个对象的析构函数得到执行并回收了内存,数组的其他对象所占内存得不到回收导致内存泄漏;
4)浅拷贝,两个指针指向同一块内存,这样析构的时候两个对象两次析构,则同一块内存空间被释放2次,造成内存泄漏.所以如果一个类里面有指针成员变量,要么必须显式地写拷贝构造函数重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
5)指针数组的情况,只释放了数组,没释放每个指针元素指向的内存空间(应先用一个循环释放掉每个指针元素指向的内存空间,再delete[] arr.

7.const辨析

在函数中,若参数用值传递,则不需要加const,加了也没用,因为函数将自动产生临时变量用于复制该参,该输入参数本来就无需保护.若用指针或引用传递且我们不希望改变参数的值,那么加上const就可以保护参数免于被改变:e.g. void Func(const A &a).
那么是否应将void Func(int x) 改写为void Func(const int&x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造,析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
故对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”以提高效率,例如将void Func(A a) 改为void Func(const A &a).


const在函数前后的辨析

○const在前:用const修饰函数的返回值.
如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针,如下:

const char * GetString(void);
const char* pt = GetString();//不能写char* pt= GetString();

但是,如果函数返回值采用值传递,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值.

○const在后:const(常)成员函数.
任何不会修改数据成员的函数都应该声明为const类型.


常成员函数的特点和注意事项:
1)const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
2)const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
3)const成员函数不可以修改对象的数据,不管对象是否具有const性质.
4)而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的(先不看了,这个不了解)
5)常成员函数的this指针为:const 类型名 *const this(不允许改变this指向的对象)而正常的this指针为:类型名* const this

8.static(静态数据成员和函数)


静态数据成员

要对静态数据成员定义和初始化必须在类的外面也就是在全局作用域中定义,如果定义不给出初值,则默认初值为0,如下:

class Test{
public:
	int GetA() const{return a;}
private:
	//静态数据成员
	static int a;
};
//int Test::a;如果这样定义不赋予初值则初值为零
int Test::a = 1;

#include <iostream>
int main()
{
	Test T;
	std::cout << T.GetA() << std::endl;
	}


静态成员函数

静态成员函数就是在类的成员函数前加上static关键字.
1)静态成员函数不能调用非静态成员函数,但是反过来是可以的
2)静态成员函数没有this指针,即静态成员函数不能使用修饰符(也就是函数后面的const关键字)

注:
1)静态数据成员可以作为成员函数的默认形参,而普通数据成员则不可以,如下:

class Test{
public:
	//静态数据成员
	static int a;
	int b;
	void fun_1(int i = a);//正确
	void fun_2(int i = b);//报错

2)静态数据成员的类型可以是所属类的类型,普通数据成员则不可以.普通数据成员的只能声明为 所属类类型的指针或引用
3)静态数据成员在const函数中可以修改,而普通的数据成员不能修改

9.单例模式

1)保证一个类只有一个实例,并提供一个全局访问点
2)禁止拷贝

示例代码:

class Singleton{
public:
	Singleton(const Singleton&)=delete;
	Singleton & operator=(const Singleton&)=delete;
	static Singleton &get_instance()
	{
		static Singleton instance;
		return instance;
	}
private:
	Singleton(){}
};

10.友元函数,友元类

1)友元函数没有this指针.
2)因为友元函数是类外的函数,所以它的声明可以放在类的私有段或公有段且没有区别.
3)友元函数不能被继承.
4)友元类:类Y的所有成员函数都是类X的友元函数–提供一种类之间合作的一种方式
友元类的所有函数都自动变为友元函数.
友元类的代码,如下:

class girl;
class boy
{
public:
  void disp(girl &);
};
void boy::disp(girl &x) //函数disp()为类boy的成员函数,也是类girl的友元函数
{
  cout<<"girl's name is:"<<x.name<<",age:"<<x.age<<endl;//借助友元,在boy的成员函数disp中,借助girl的对象,直接访问girl的私有变量
}
class girl
{
private:
  char *name;
  int age;
  friend boy; //声明类boy是类girl的友元
};

n.一些杂碎知识点

☆this指针的类型:类名*const
多态性是将接口与实现进行分离
作用域限定符为::
f(0)这个表示方法中,0可以表示数值也可以表示空指针

构造函数和析构函数不会被子类继承,在子类的析构函数中会调用父类的析构函数.子类的构造函数会默认调用父类的无参构造函数.
赋值运算符重载函数也不会被子类继承,只是在子类的赋值运算符重载函数中会调用父类的赋值运算符重载函数(???先记住)

按值传递会调用拷贝构造函数,按引用传递不会.

  • 59
    点赞
  • 396
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值