Cpp复习

cpp问题总结

C基础

程序形式:程序=过程+调用

I>自顶向下、逐步求精:

I>模化:

II>语句结构化

image-20211205182046698
image-20211205182046698
1
2
3
4
int a=0
int a(0)
int a={0};
int a{0};
1
2
//简单的I/O格式控制
setw(5)//设置域宽

swith分支的判断都是char或者int

1、数组名是一个常量,不能被赋值

2、二维数组初始化

1
2
3
4
5
6
7
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//部分初始化化
int a[3][4]={{1},{0,6},{0,0,11}};
//列出全部初始值时,第1维下标个数可以省略
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int a[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

数组中a[],a作为数组名,不能进行a++,但是指针p=a+10是可以的

1
2
while(--n)//先自减,再判断n==0
while(n--)//先判断n==0,再自减

C语言中的变量声明必须集中的在前面,而不能放在函数执行的过程之中,需要我们特别注意

1
2
3
4
5
f()
{
int i;
i=10;//这是不允许的
}

带有默认参数的函数,默认参数需要放在后面

面向对象概述

1、什么是面向对象?

程序=对象+消息

2、什么是对象,对象与类的关系是什么?

在面向对象程序设计中,对象是描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体。在C++中每个对象都是由数据和操作代码(通常用函数来实现)

在面向对象程序设计中,“类”就是具有相同的数据和相同的操作(函数)的一组对象的集合,也就是说,类是对具有相同数据结构和相同操作的一类对象的描述。

两部分组成的,类和对象是抽象与具体的关系,类是多个对象综合抽象的结果,对象是类的具体实例

3、什么是消息?

一个对象向另一个对象发出的请求称为消息

消息有三个性质:

(1)同一个对象可以接收不同形式的多个消息,作出不同的响应;

(2)相同形式的消息可以传递给不同的对象,所作出的响应可以是不同的;

(3)对消息的响应并不是必需的,对象可以响应消息,也可以不响应。

4、什么是抽象与封装?

抽象是将有关事物的共性归纳,集中的过程,是对现实世界的简洁表达方式,抽象包括两个方面,数据抽象和代码抽象。

封装是把某个事物包围起来,使外界不知道该事物的具体内容,将数据和实现操作的代码集中放在对象的内部,尽可能屏蔽对象的内部细节

5、什么是继承?

继承所表达是类之间的相关关系,这一关系使得某类对象可以继承另外一类对象的特征和能力

6、派生类和父类(继承)有什么特征?

(1)类间具有共享特征(包括数据和操作代码的共享);

(2)类间具有差别或新增部分(包括非共享的数据和操作代码);

(3)类间具有层次结构。

7、什么是单继承、多继承

单继承是指每个派生只是继承了一个基类的特征

多继承是指多个基类派生出一个派生类的继承关系

8、什么是多态? 不同的对象受到同样消息执行不同操作,有两种多态

  • 函数重载,运算符重载(编译时的多态)
  • 虚函数(运行时的多态)

9、面向对象的优点是什么?

(1)可提高程序的重用性;(继承、模板)

(2)可控制程序的复杂性;

(3)可改善程序的可维护性;

(4)能够更好地支持大型程序设计;

(5)增强了计算机处理信息的范围;

(6)能很好地适应新的硬件环境。

cpp概述

1、简述cpp的特点?

  • cpp是c的超集,cpp保持了与c的兼容,使得众多c的函数库和代码不经修改可以直接用于cpp中
  • C++是一个更好的C,它保持了C的简洁、高效和接近汇编语言等特点,并对C的功能作了不少扩充。用C++编写的程序比C更安全,可读性更好,代码结构更为合理,C++的编译系统能够检查出更多的类型错误。
  • 用C++编写的程序质量高,从开发时间、费用到形成的软件的可重用性、可扩充性、可维护性和可靠性等方面有了很大的提高,使得大中型的程序开发变得更加容易。
  • 增加了面向对象的机制,支持几乎所有面向对象的特征其中包括:抽象与封装、继承、多态、模板
  • cpp既可以用于面向过程的结构化程序设计,也可以用于面向对象程序设计

2、这个注释是否可用(cpp中嵌入c的注释)

1
// this is my first note/*way to my home*/

可以

3、这个程序是否能编译?

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
using namespace std;
//int sum(int a,int b);
int main()
{
int a=1,b=2;
cout<<sum(a,b);
}
int sum(int a,int b)
{
return a+b;
}

不能,因为没有函数原型声明

4、这两个函数是否等价

1
2
float fun(int a, float b,char * C)
float fun (int, float,char* )

不等价,必须包含参数名

5、常指针和常量指针

常指针

1
2
3
char * const name="chen";
name[3]='q';//正确
name="yu";//不正确

常量指针

1
2
3
const char * name ="chen";
name[3] ='q' ; //不正确
name="yu";//正确

6、内联函数不是可以包含各种语句,其中例如forswitch都不行,内联函数在第一调用前必须有完整的定义,如果没有,也需要声明

1
inline int box(int ,int );

7、

  • 引用的对象不能是数组,不能创建引用的引用
1
2
3
int && a=b; //引用的引用是不允许的
a=&b;
c=&b; //这种引用传递是可以的
  • 数组使用new创建数组也不能直接初始化,但是基本数据类型可以,例如
1
2
int *p = new int(10);//初始化int指针为10
int *p = new int[10];//初始化长度为40的数组

8、函数重载不能根据其返回的数据类型来判断,而是参数的个数,参数的数据类型(形参表不一样)

9、用new创建对象时也需要调用构造函数,删除时可以使用delete []name,并且一个指针也只能调用一次delete

1
2
3
4
5
6
7
8
9
10
11
12
//用 new 创建时调用构造函数
class A{
public:
A(){
cout<<"construct"<<endl;
}
};
int main()
{
A *p=new A;
return 0;
}
  • delete是会释放内存的,但是不会让指针变量消失,也就是,它还可以指向别的地址
1
2
3
4
5
int *p = new int[10];
delete []p;
int a[]={1,2,3};
p=a;
cout<<p[1];

类与对象

1、类默认的私有成员属性让整个cpp中的类比结构体更安全(数据隐藏),但是结构体默认是公有的

  • 不能在类声明中给数据成员赋初值
  • 在声明类的同时定义的对象是一种全局对象,它的生存期直到整个程序运行结束
  • 声明了一个类便声明了一种类型,这时没有给它分配存储空间

2、类外定义成员函数

1
2
3
4
5
6
7
8
9
class Point{
inline void s();
void b();
...
}
inline void Point::s()//定义内联函数
{
}
void Point::b()//定义普通函数

3、定义对象的三种方式

1
2
3
4
5
6
class Point{
...
}a,b,c;//方式一

Point a,b,c;//方式二
Complex *p =new Complex(1,2)//无名对象

4、指针访问对象成员的两种方式

1
2
ptr->a;
(*ptr).a;//这种容易忘记

5、构造函数和析构函数(构造函数在类外定义时一定要记得加类名,无需返回值

功能:为对象分配存储空间,为对象收回存储空间

  • 构造函数:名字与类名同名,参数任意,不能有返回值,可以类外定义,构造函数不能手动调用
1
2
3
4
5
6
7
class a{
....
}
{
a b;
b.a();//错误,不能主动调用
}
  • 析构函数:

6、对象初始化

在定义对象时,对数据成员赋初值,称为对象的初始化。在定义对象时,如果某一数据成员没有被赋值,则它的值是不可预知的。对象是一个实体,在使用对象时,它的每一个数据成员都应该有确定的值

1
2
3
4
5
6
class Complex{
public:
double real;
double imag;
};
Complex c1={1.1,2.2} //这种初始化一定要注意了,有私有成员时不行,只有公有成员可以

7、成员初始化列表对数据成员初始化

必须使用该方法的两类数据,一是常量const,二是引用&.

1
Complex(double x,double y):real(x),imag(y){};//构造函数后面操作

数据成员是按照它们在类中声明的顺序进行初始化的,与它们在成员初始化列表中列出的顺序无关

注意:如果参量是数组,则一定不能使用初始化表对其进行初始化

8、无参构造函数对象的定义

1
2
Complex a;	//正确
Complex a();//错误

9、带默认参数的构造函数

(1)如果构造函数在类的声明外定义,那么默认参数应该在类内声明构造函数原型时指定(默认参数只能放在后面),而不能在类外构造函数定义时指定。

(2)在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数。例如在一个类中有以下构造函数的声明:

1
2
Complex(double x=0,double imag=0);
Complex(double r);

10、析构函数(如果函数传参是对象引用,则不会调用析构,如果是对象,则会调用,但返回值有需要另外分析

(1)析构函数不返回任何值。在定义析构函数时,是不能说明它的类型的,甚至说明为void类型也不行

(2)析构函数没有参数,因此它不能被重载。一个类可以有多个构造函数,但是只能有一个析构函数。

new出来的空间需要在析构函数中delete释放,针对类中的指针变量,

(3)使用new生成的对象delete时也需要调用析构函数

11、对象数组

  • 对象数组的赋值

(1)创建多大的对象数组,就要调用构造函数多少次

1
2
3
4
5
6
7
8
9
10
//单参数
class a....;
{
a c[4]={1,2,3,4};//构造函数调用四次(单个参数才可以)
}
//a中有空构造函数时,下面也可以
{
a c[4];
c[1]=a(1);
}

(2)对象数组的赋值可以使用多类构造函数赋值

1
2
3
4
5
6
7
class a{
a();
a(int i);
}
{
a c[4]={1,2};//多类构造函数,一个是a().一个是a(int)
}

(3)定义数组时实参的个数不能超过数组元素的个数

1
2
3
{
a c[4]={1,2,3,4,5,6}//错误
}

(4)多参数构造函数对象数组的赋值

1
2
3
{
a c[4]={a(1,2),a(2,3),a(3,4),a(3,5)};//多参数的唯一赋值赋值方式
}

(5)对象指针访问对象数组

1
2
3
4
5
6
7
{
a *p,c[4]..;
p=c;
p[0]->show();
p++;//c[1]
p->show();
}
  • 对象的赋值和复制

  • 拷贝复制函数(这个函数没有返回值

1
2
Point p2(p1)//使用拷贝复制函数构建p2,方式一
Point p2=p1 //使用拷贝复制函数构建p2,方式二

(1)与构造函数同名

(2)只有一个参数,参数类型是自身引用

(3)每个类有会有拷贝构造函数

1
2
3
4
5
6
//自定义拷贝复制函数
class Point{
Point(const Point &p){
.....
}
}
  • 调用拷贝构造函数的情况(如果是在派生类中,会调用base析构函数,不一定调用base构造函数(除了自定义了拷贝构造函数外))
1
2
3
4
5
6
7
8
9
10
11
12
13
class A;
class B
{
B(const B&){}
}
B b1;B b2(b1); //这里b2的产生会调用A的构造
//默认拷贝构造函数
class A;
class B
{
// B(const B&){}
}
B b1;B b2(b1); //这里b2的产生不会调用构造

(1)用一个对象初始化一个对象

(2)函数的形参是对象

(3)函数的返回值是对象

这个返回值时对象的情况,实际上还调用了赋值函数

静态成员(属于类,而不是对象)

静态数据成员(在类函数里可以直接访问,无需加上类名)需要作用域初始化

1
2
3
4
5
6
7
static int a;//类内声明
//初始化时需要int放在前面
int A::a=0;//main函数之前初始化,不用static前缀//需要作用域
A::a; //类访问方式
A b;
b.a;//对象访问方式
p->a;//指针访问方式

静态函数成员

1
2
3
static int f(int a);
//访问方式同数据成员
int A::f(int a){}//不用static前缀

静态函数成员没有this指针,这是它和其它函数最大的区别,一般来说,不访问非静态成员,如果访问,则也是需要通过对象的引用

1
2
3
4
class A{
static void dis(A &w);
}
void A::dis(A &w){....}

拷贝构造函数的调用关系

  • 当子类的拷贝构造函数没有自定义的时候,拷贝时默认会调用父类的拷贝构造函数(无论父类构造是否自定义)
  • 当子类的拷贝构造函数自定义之后,拷贝时默认调用父类的构造函数,这时父类的数据成员达不到复制的目的,需要显式调用父类拷贝构造函数。
1
2
3
4
5
6
7
8
9
class A{
public:
...
};
class B:public A{
public:
B(){};
B(const B &ob):A(ob){...};
};

友元

为什么要使用友元函数

(1)友元函数通过直接访问对象的私有成员,提高了程序运行的效率。

(2)在某些情况,如运算符被重载时,需要用到友元函数。

(3)一个函数可以是多个类的友元函数。当一个函数需要访问多个类时,友元函数非常有用

(1)友元关系是单向的,不具有交换性。若类X是类Y的友元,类Y是否是X的友元,要看在类中是否有相应的声明。

(2)友元关系也不具有传递性。若类X是类Y的友元,类Y是类z的友元,不定类X是类Z的友元。

普通友元函数

(1)友元一定不是当前类的成员函数

(2)只需要在声明的时候前面加上friend,可以定义在类的内部,也可以定义在类的外部

1
2
friend void si();
void si(){};//声明在类内,定义在类外,不是成员函数

友元在访问的时候,必须传入对象指针或者对象的引用来进行访问

友元函数是其它类的成员函数

1
2
3
4
5
6
7
8
9
10
//友元成员函数
class Girl;//这个很必要
class Boy{
public:
void si(Girl &);
}
class Girl{
public:
friend void Boy::si(Girl &);
}

(1)一定要先定义友元函数所在的那个类,在定义声明为友元的那个类,需要提前声明类是存在的

友元类

(1)友元类(友元函数所在的类)必须在最开始定义,在其它类里面声明friend 类名

1
2
3
4
5
6
7
class Girl;
class Boy{
print(Girl &);
}
class Girl{
friend (class) Boy; //class 可以有,可以没有
}

如果需要互相声明为友元,则需要在上面的类中加上声明下面的类为友元即可,声明友元类后,友元内的成员函数不能够在类内定义,只能类外定义.

类的组合

在一个类中有其它类对象的变量

初始化的方法:初始化化表

1
2
3
4
5
class A{
B b;
C c;
A(int b1,int c1):b(b1),c(c1){};
}

构造顺序只与其在类中定义的顺序有关,与其在初始化表中的顺序无关,析构相反,先调用A->调用B->调用c->执行A的函数体

常类型

  • 常引用

const 类型 & 引用名作为形参

  • 常对象

const 类名 对象名(参数表),或者类名 const 对象名(参数表)

1
2
3
A const a;
a.changevalue();//报错,常对象的数据成员不允许变更
a.dis();//常对象成员不可以调用普通函数
  • 常函数

返回类型 函数名(参数表)const;

1
2
3
4
5
void dis() const;
void A::dis const()
{
....
}

常对象只能调用常函数,不能调用普通函数,但是常函数可以被任意调用,常函数可以调用对象的数据成员,但是不能该对象的数据成员,可以改变参数表传进来的形式参数

【3.1】类声明的一般格式是什么?

【解】类声明的一般格式如下:class类名 private:私有数据成员和成员函数 public公有数据成员和成员函数 其中:class是声明类的关键字,类名是要声明的类的名字;后面的花括号表示类声明的范围;最后的分号表示类声明结束 除了 private和 public之外,类中的成员还可以用另一个关键字 protected来说明。这时类声明的格式可写成:

类名

[private:]私有数据成员和成员函数

public:公有数据成员和成员函数

protected:保护数据成员和成员函数

被 protected说明的数据成员和成员函数称为保护成员。保护成员可以由本类的成员函数访问,也可以由本类的派生类的成员函数访问,而类外的任何访问都是非法的,即它是半隐蔽的。

【3.2】构造函数和析构函数的主要作用是什么?它们各有什么特性?

【解】构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值类型。它不需要用户来调用,而是在建立对象时自动执行

构造函数具有一些特性:

(1)构造函数的名字必须与类名相同,否则编译程序将把它当作一般的成员函数来处理。

(2)构造函数没有返回值,在定义构造函数时,是不能说明它的类型的,甚至说明为void类型也不行。

//(3)构造函数的函数体可写在类体内,也可写在类体外。

//(4)构造函数的作用主要是用来对对象进行初始化,用户根据初始化的要求设计函数体和函数参数。在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句,但是,为了保持构造函数的功能清晰,一般不提倡在构造函数中加入与初始化无关的内容。

(5)构造函数一般声明为公有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用的,而且只执行一次。

(6)在实际应用中,通常需要给每个类定义构造函数。如果没有给类定义构造函数,则编译系统自动地生成一个默认构造函数。

析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于执行些清理任务,如释放分配给对象的内存空间等。析构函数有以下一些特点:

(1)析构函数名与类名相同,但它前面必须加一个波浪号(~)。

(2)析构函数不返回任何值。在定义析构函数时,是不能说明它的类型的,甚至说明为void类型也不行。

(3)析构函数没有参数,因此它不能被重载一个类可以有多个构造函数,但是只能有一个析构函数

(4)撤销对象时,编译系统会自动地调用析构函数

【3.3】什么是对象数组?

【解】所谓对象数组是指每一数组元素都是对象的数组,也就是说,若一个类有若干个对象,我们把这一系列的对象用一个数组来存放。对象数组的元素是对象,不仅具有数据成员,而且还有函数成员

【3.4】什么是this指针?它的主要作用是什么?

【解】C++为成员函数提供了一个名字为this的指针,这个指针称为自引用指针。每当创建一个对象时,系统就把this指针初始化为指向该对象,即this指针的值是当前被调用的成员函数所在的对象的起始地址。每当调用一个成员函数时,系统就自动把this指针作为一个隐含的参数传给该函数。不同的对象调用同一个成员函数时,C++编译器将根据成员函数的this指针所指向的对象来确定应该引用哪一个对象的数据成员

【3.5】友元函数有什么作用?

【解】友元函数不是当前类的成员函数,而是独立于当前类的外部函数但它可以访问该类所有的成员font>,包括私有成员、保护成员和公有成员。当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相应的所有类的数据。此外,在某些情况,例如运算符被重载时,需要用到友元函数

1
2
Point p1=p2;//调用拷贝构造
p3=p2 //调用默认赋值

杂项:

(1)不能在类声明中给数据成员赋值,但是可以通过构造函数等

(2)静态成员函数可以在类内或类外定义,但是静态成员只能在类内定义

(3)友元函数既可以在类内定义,也可以在类外定义

(4)一定要记住,如果函数中使用了对象,那么在函数结束时一定会调用析构函数,作为函数参数时不调用构造函数,而是调用拷贝构造函数

(5)下面这种情况调用默认赋值函数

1
2
A a1,a2;
a1=a2 //这时候是不调用拷贝构造函数的!!!

(6)数组传参的本质是地址,也就是说,只能用指针进行接收,若是字符串,则可以这样

1
2
3
4
5
6
#include<string.h>
void f(char a[])
{
char b[90];
strcpy(b,a);
}

派生与继承

(1)派生类不继承基类的构造函数和析构函数

(2)调整基类的成员属性

  • 三种继承方式

    访问声明

    1
    2
    3
    4
    5
    6
    class A;
    class B{
    public:
    A::print //记住,这里不用括号,不带返回类型,不带变量
    A::a;//调整成员数据,形式一致
    };

    调整只能对应调整,即基类中的公有->子类中的公有;基类中的保护->子类中的保护,私有成员不能调整(始终不可访问),对所有的基类重载函数都起作用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class A
    {
    public:
    int x1;
    protected:
    int x2;
    private:
    int x3
    }
    class B
    {
    public:
    A::x1;
    protected:
    A::x2;
    }

    记住下面着这个例子

    1
    2
    3
    class A;
    class B;
    class C:public A,B{};

    其中对A是公有继承,对B是私有继承。

  • 同名成员(重定义)

    (1)函数重定义(可以直接调整函数原来的属性)

    要求:函数名一样,参数表一样(参数可以不一样,这时候会直接隐藏父类的函数):覆盖 , 如果不一样,则就是重载覆盖不代表原来的函数没有了,它其实还是存在的,只是这时访问要加上作用域

    1
    2
    3
    4
    5
    6
    class X;
    class Y;
    Y::m{
    f();//访问y中的f()
    X::f();//访问x中的f()
    }

    (2)数据成员重写(这种方式可以被直接无视父类中成员的权限,直接提升权限

(3)基类中的私有成员。无论哪种继承方式,基类中的私有成员不允许派生类继承,即在派生类中是不可直接访问的。但是私有继承时,原来的public \ protected都是可以被内部访问的

(4)基类中的私有成员既不能被派生类的对象访问,也不能被派生类的成员函数访问,只能被基类自己的成员函数(公有或者protected)访问

(5)通常情况下,当创建派生类对象时,首先执行基类的构造函数,随后再执行派生类的构造函数;当撤销派生类对象时,则先执行派生类的析构函数,随后再执行基类的析构函数。

(6)当基类含有带参构造函数时,派生类必须定义构造函数给基类构造函数传参.再多文件编程时,需要在类的内部写上函数声明(不包括哪些参数表),在类的外部写上调用基类构造函数,以及列表初始化等参数。

(7)指针定义的无名继承对象,如果是子类指针,则会调用子类、父类的析构函数,但是如果指针是父类指针,则在回收时会调用父类析构函数,并且,一旦定义了指针,一定是要有delete释放,即使main函数结束,指针变量依然不会释放。

派生类中的构造函数的调用顺序

1
2
//B是A的子类
B():A(...),a1(..),a2(..){}
  • 调用基类构造函数
  • 调用内嵌对象的构造函数
  • 执行B的函数体
  • 析构顺序相反

对象成员的构造函数调用顺序是他们在派生类中的声明顺序,析构相反

多重继承

形式:

1
2
3
4
5
6
class A;
class B;
class C:public A,protected B
{
....//先调用A的构造,再调用B的构造
};

构造函数的调用顺序与声明时的顺序有关,析构相反

虚基类

(1)如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚 基类构造函数的调用(不是虚基类不用调用。只用管自己的上一级),以初始化在虚基类中定义的数据成员。

(2)建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的(构造函数只执行一次,普通继承调用多次)。该派生类的其他基类 对虚基类构造函数的调用都自动被忽略。

(3)若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非 虚基类的构造函数,最后调用派生类构造函数

1
2
class X:public Y virtual public Z{};
//先调用z,然后y,然后x

(4)对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。

(5)对于非虚基类,构造函数的执行顺序仍是先左后右,自上而下。

(6)若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造

兼容赋值

1
2
3
4
5
Base b;
Derieve d;
b=d;//只能访问b具有的成员
Base &b=d;
base *p=&d;

指向基类的指针只能指向他的公有派生类的对象,而不能指向他的私有和保护派生对象。

【4.3】保护成员有哪些特性?保护成员以公有方式或私有方式被继承后的访问特性 如何?

【解】当类的继承方式为公有继承时,基类中的所有保护成员在派生类中仍以保护成 员的身份出现,在派生类内可以访问这些成员,但派生类外部不能访问它们,而在下一层派 生类内可以访问它们。

当类的继承方式为私有继承时,基类中的所有保护成员在派生类中都以私有成员的身 份出现,在派生类内可以访问这些成员,但派生类外部不能访问它们。

【4.7】在类的派生中为何要引人虚基类?虚基类构造函数的调用顺序是如何规定的?

【解】当引用派生类的成员时,首先在派生类自身的作用域中寻找这个成员,如果没有 找到,则到它的基类中寻找。如果一个派生类是从多个基类派生出来的,而这些基类又有一 个共同的基类,则在这个派生类中访问这个共同的基类中的成员时,可能会产生二义性。为 了解决这种二义性,C++引入了虚基类的概念。 虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:

(1)如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则 整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚 基类构造函数的调用,以初始化在虚基类中定义的数据成员。

(2)建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是 由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类 对虚基类构造函数的调用都自动被忽略。

(3)若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非 虚基类的构造函数,最后调用派生类构造函数。

(4)对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。

(5)对于非虚基类,构造函数的执行顺序仍是先左后右,自上而下。

(6)若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造 函数。

多态

1、运算符重载(所有的运算符重载都有返回值(除了转化构造函数外,一般为对象,但是=和[]返回的值对象的引用),一般都需要传入对象的引用,双目运算符函数的提供者是左边,单目运算符提供者默认是右边)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
class Complex{
....
};
Complex operator+(Complex a,Complex b)
{
....
}
int main()
{
total1=com1+com2; //调用方式1
total2=operator+(com1,com2); //调用方式2
}

不能重载的运算符有:

1
2
3
4
5
.
.*
::
sizeof
?:

C++不允许自己定义新的运算符,例如**不能重载,也不能定义,因为C++中无法通过这个进行乘方运算

  • 重载运算符不能改变操作数的个数(+)
  • 重载不能改变优先级(+,\(\times\)
  • 重载不能改变结合特性
  • 重载的参数至少一个是类对象(类引用),不能全部是标准数据类型
  • =不用重载,除非类中有动态分配内存的指针成员

友元运算符函数重载

  • 友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数(一定不是自己的成员函数)
1
2
3
4
5
6
7
8
9
//友元运算符重载
class x{
friend 函数类型 operator运算符(形参表);
...
};
函数类型 operator运算符(形参表) //无需加上类名
{
...
}
  • 在重载单目运算符,特别是++和--的时候,一定要记得使用对象引用,因为对象类型将无法改变它自身
  • 有些的运算符不能定义为友元重载函数如= 、[] 、(),他们只能定为。。。

成员运算符重载

  • 双目运算符:参数表为一个操作数
1
2
3
4
5
6
7
8
9
10
11
//测试访问权限
class x{
public:
....
x operator*(x ob);
};
x x::operator*(x ob)
{
real=real*ob.real;
imag=imag*ob.imag;//可以直接访问ob.imag
}

可以看出,即使*做为x的成员函数,但是它还是可以访问同类型的ob的数据,不存在权限不足的情况。实际调用左边对象的成员函数

1
2
3
//调用方式
a.operator@(b)
a@b
  • 单目运算符:参数表为空,这是适用于前缀
1
2
3
4
5
6
7
8
9
class coord{

coord operator++()
};
coord coord::operator++()
{
++x;
++y;
}
  • 单目运算符:参数为int,适用于后缀
1
2
ob.operator++(int)	//成员函数重载
operator(x &ob,int) //友元函数重载,一定要是引用
  • 前后缀对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//声明区别
//前缀
x operator++();
friend x operator++();
//后缀
x operator++(int); //无需参数名,只需写类型
friend x operator++(int);
//定义区别
//前缀
x x::operator++(){}
x operator++(){}
//后缀
x x::operator++(int){}
x operator++(int){}
//调用区别
//前缀
ob.operator++();//显式调用,成员函数
operator++(ob);//显示调用,友元函数
++ob; //隐式调用:友元重载和成员函数重载一样

//后缀
ob.operator++(0);//显式调用,成员函数
operator++(ob,0);//显示调用,友元函数
ob++; //隐式调用:友元重载和成员函数重载一样

前缀++x是先自加,在进行下一步运算,x=0,z=x++*4,则最终z=0.

双目运算符一般可以被重载为友元运算符重载函数或成员运算符重载函数,但有一种情况,必须使用友元函数 例如,如果将一个复数与一个整数相加,可用成员运算符函数重载“+”运算符:

1
2
3
Complex A::operator+ (int a)
{ return Complex(real+a ,imag);
}// obj+100可以 100+obj不可以

但是通过声明友元即可以解决这个问题,声明两个友元函数即可,即如果操作数希望有隐私类型转化,必须使用友元函数重载。

1
2
3
4
friend Complex operator+(comp1 ex com,inta)/运算符+的左侧是类对象
{return Complex(com.real+ a,com.imag)}//右侧是整数
friend Complex operator+(inta,Complex com)//运算符+的左侧是整数
{return Complex(a+ com.real,com.imag)}//右侧是类对象
  • 指针悬挂问题

当使用自带的=的时候,指针赋值容易出现问题,那是因为,在使用析构函数进行回收内存的时候,会导致原来指针的空间被回收,=只能定义为成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class a
{
private:
char *ptr;
public:
~a(){
delete ptr; //如果这里没有delete,则这一块空间不会被回收,即使是该对象已经消亡
...
}
}
int main()
{
a a1(..),a2(..);
a2=a1; //a2.ptr=a1.ptr
return 0; //a2先消失,a1后消失,一定要记住
}

如何解决,使用成员函数重载(深层赋值)

1
2
3
4
5
6
7
String &String::operator=(const String &x)
{
delete ptr;//释放空间,但是没有删除变量
ptr=new char[strlen(x.ptr)+1];
strcpy(ptr,x.ptr);
return *this;
}
  • 下标运算符只能定义为成员函数

类型转化

  • 转化构造函数(只能是类内构造函数),没有说明返回类型
1
2
3
4
5
6
class a{
a(){};
a(double r){ //要转化的类型,转化构造函数
x=r,y=0;
}
};

(1)转化构造函数只有一个参数,作用是将一个其他类型的数据转换成它所在类的对象

(2)转换构造函数不仅可以将一个系统预定义的标准类型数据转换成类的对象,也可以将另一个类的对象转换成转换构造函数所在的类对象。

  • 类型转化函数(只能是类的成员函数没有返回类型,只有operator关键字,无需参数表,但一定有return,记住了

将类对象转化为另一类数据->类转化为其它数据

1
2
3
4
5
6
7
8
9
10
11
class Complex{
operator double()//目标类型
{
return real;
}
operator int()
....
}
{
double(com) //调用方式,注意这个和一般函数调用的区别
}

(1)类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。类型转换函数也可以在类体中声明函数原型,而将函数体定义在类的外部

(2)类型转换函数既没有参数,也不能在函数名前面指定函数类型

(3)类型函数中必须有 return语句,即必须送回目标类型的数据作为函数的返回值。

(4)一个类可以定义多个类型转换函数。C++编译器将根据类型转换函数名自动地选择一个合适的类型转换函数予以调用。

1
2
3
4
5
6
7
8
9
//类型转化隐式调用
class Complex{
Complex(int i);
operator int();
}
{
Complex a1(1,2),a2(2,3),a3;
a3=a1+a2;//先调用类型转化函数,在调用转化构造函数
}

虚函数

虚函数是重载的另一种表现形式。这是一种动态的重载方式,它提供了一种更为灵活的运行时的多态性机制。属于动态联编

原来,在C++中规定:基类的对象指针可以指向它的公有派生的对象,但是当其指向公有派生类对象时,它只能访问派生类中从基类继承来的成员,而不能访问公有派生类中定义的成员。

1
2
3
4
5
6
7
8
9
10
11
class a{
print1();
}
class b:public a{
print2()
}
{
a *p=&b;
p->print1();//正确
p->print2();//错误
}

可以通过基类的指针或者引用来访问同名函数,在派生类中重新定义时,其函数原型,包括函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。不一样的话将直接调用基类的虚函数

(1)若在基类中,只声明虚函数原型(需加上 virtual),而在类外定义虚函数时,则不必再加 virtual

(2)virtual可写可不写,在子类中

(3)如果虚函数没有被重新定义,则自接继承父类虚函数

(3)虚函数必须是类的成员函数不能是友元函数,不能是静态函数

1
2
3
4
5
6
7
8
9
10
11
//引用调用虚函数
class a{
virtual void print(){}
}
class bpublic a{
virtual void print(){}
}
{
a &qq=b;
a.print();//具有虚函数的特点,调用class b的函数
}
  • 虚析构函数

不能声明虚构造函数,但是可以声明虚析构函数,若是按照上面的a,b两个类

1
2
3
4
5
6
7
8
9
10
{
b obj;
return 0;//即调用先调用b的析构,在调用a的析构
}
{
a *p;
p=new b;
delete p;//只调用a的析构,不调用b的析构
return 0;
}

如果使用虚析构函数,则可以达到先调用b的析构,再调用a的析构

1
2
3
4
5
6
7
8
9
10
11
a{
virtual ~a(){
...
}
}
{
a *p;
p=new b;
delete p;//先调用b的析构,在调用a的析构
return 0;
}

虚函数是函数重载的另一种形式,但它不同于一般的函数重载。

普通的函数重载:其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。

虚函数:要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。

1
2
3
4
5
6
7
class a
{
void ab()
}
class b:public a{
void ab()//普通函数重载,相当于函数重写
}
  • 纯虚函数与抽象类
1
virtual void area()=0;

(1)不能建立抽象类对象,只能作为基类

(2)可以定义指针,指向子类,不能作为参数类型,函数返回类型

问题

1、什么是动态联编和静态联编?

​ 静态联编是指系统在编译时就决定如何实现某一动作。静态联编要求在程序编译时就知道调用函数的全部信息,因此,这种联编类型的函数调用速度很快。效率高是静态联编的主要优点。 ​ 动态联编是指系统在运行时动态实现某一动作。采用这种联编方式,一直要到程序运行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性和程序易维护性

2、编译时的多态性与运行时的多态性有什么区别?它们的实现方法有什么不同?

【解】静态联编支持的多态性称为编译时多态性,也称静态多态性。在C++中,编译时多态性是通过函数重载(包括运算符重载)和模板实现的。利用函数重载机制,在调用同名的函数时,编译系统可根据实参的具体情况确立所要调用的是哪个函数

动态联编所支持的多态性称为运行时多态性,也称动态多态性。在C++中,运行时多态性是通过虚函数来实现的。

3、简述运算符重载的规则。

【解】C++语言对运算符重载制定了以下一些规则:

(1)C++中绝大部分的运算符允许重载,不能重载的运算符只有少数几个。

(2)C++语言中只能对已有的C++运算符进行重载,不允许用户自己定义新的运算符。

(3)运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造完成的。一般来讲,重载的功能应当与原有的功能相类似(如用“+”实现加法,用“-”实现减法)

(4)重载不能改变运算符的操作对象(即操作数)的个数。

(5)重载不能改变运算符原有的优先级。

(6)重载不能改变运算符原有的结合特性。

(7)运算符重载函数的参数至少应有一个是类对象(或类对象的引用)。

(8)运算符重载函数可以是普通函数,也可以是类的成员函数,还可以是类的友元函数。

(9)一般而言,用于类对象的运算符必须重载,但是赋值运算符“=”例外,不必用户进行重载。但在某些情况下,例如数据成员中包含指向动态分配内存的指针成员时,使用系统提供的对象赋值运算符函数就不能满足程序的要求,在赋值时可能出现错误。在这种情况下,就需要用户自己编写赋值运算符重载函数。

【5.4】友元运算符重载函数和成员运算符重载函数有什么不同

【解】友元运算符重载函数和成员运算符重载函数的不同有以下几点: (1)对双目运算符而言,成员运算符重载函数参数表中含有一个参数,而友元运算符重载函数参数表中含有两个参数;对单目运算符而言,成员运算符重载函数参数表中没有参数,而友元运算符重载函数参数表中含有一个参数 (2)双目运算符一般可以被重载为友元运算符重载函数或成员运算符重载函数,但有些情况,必须使用友元运算符重载函数,例如一个常数与一个对象相加。有的运算符(如=等)只能使用成员运算符重载函数

(3)成员运算符函数和友元运算符函数都可以用习惯方式调用,也可以用它们专用的方式调用。

【5.5】什么是虚函数?虚函数与函数重载有哪些相同点与不同点?

【解】虚函数就是在基类中被关键字 virtual说明,并在派生类中重新定义的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

​ 在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般的函数重载。当普通的函数重载时,其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。但是,当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。如果仅仅返回类型不同,其余均相同,系统会给出错误信息;若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失

【5.6】什么是纯虚函数?什么是抽象类?

【解】纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求在它的派生类中定义自己的版本,或重新说明为纯虚函数。

​ 声明纯虚函数的一般形式如下:virtual 函数类型 函数名(参数表)=0;纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行重新定义。纯虚函数没有函数体,它最后面的“=0”并不表示函数的返回值为0,而只起形式上的作用,告诉编译系统“这是纯虚函数”。纯虚函数不具备函数的功能,不能被调用。

  • 当父类的构造函数为空时,可以不用在子类构造中调用父类构造函数,实际上系统会默认调用。