C++需要注意的知识

1、2、3、4原文链接:https://blog.csdn.net/fjhugjkdsd/article/details/105281651

C++中的构造函数

C++中的构造函数可以分为4类:

1、默认构造函数,又名缺省构造函数,以Student类为例,默认构造函数的原型为(无参构造函数)

Student();    //没有参数

2、初始化构造函数(有参构造函数)

Student(int num, int age;    //有参数

3、拷贝构造函数

Student(Student&);    //形参是本类对象的引用

4、移动构造函数

Student(int &&r);    //形参是其他类型变量,且只有一个形参

1.什么是类的6个默认成员函数?

如果一个类中什么成员都没有,简称为空类。任何一个类在我们不写成员函数的情况下,都会自动生成下面6个默认成员函数。

  1. 构造函数;
  2. 析构函数;
  3. 拷贝构造;
  4. 赋值重载;
  5. 普通对象取地址;
  6. const对象取地址。

2.拷贝构造函数的参数

拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

3.const修饰类的成员函数

const修饰的类成员函数称之为const成员函数。const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

观察下面代码,思考下面问题。

class Date
{
public:
	void Display ()
	{
		cout << "Display ()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl ;
	}
	void Display () const
	{
		cout << "Display () const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
};

void Test ()
{
	Date d1 ;
	d1.Display ();
	const Date d2;
	d2.Display ();
}

  1. const对象可以调用非const成员函数吗?
    答:不可以,const对象为只读,不可以将对象变为可读可写。权限不可以放大。

  2. 非const对象可以调用const成员函数吗?
    答:可以,非const为可读可写,可以将对象变为只读。权限可以缩小。

  3. const成员函数内可以调用其它的非const成员函数吗?
    答:不可以,原因同上。

  4. 非const成员函数内可以调用其它的const成员函数吗?
    答:可以,原因同上。

4.取地址及const取地址操作符重载

这两个默认成员函数一般不重新定义 ,编译器默认会自动生成。

class Date
{
public:
	Date* operator&()
	{
		return this ;
	}
	const Date* operator&()const
	{
		return this ;
	}
private:
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
};

5.向上转型和向下转型

1.子类转换为父类:向上转型,不用类型转换,使用dynamic_cast(expression),这种转换相对来说比较安全不会有数据的丢失;

Father f = new Son();
  1. 父类转换为子类:向下转型,使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。
Son s = (Son) f

6.为什么构造函数不能声明为虚函数?

1.当创建一个子类对象时,如果将构造函数声明为虚函数,因为子类在构造时,会先调用父类的构造函数,但是子类已经覆盖了基类的构造函数(因为是虚函数),所以也就无法进一步执行,导致程序出错。

2.虚函数需要通过虚函数指针指向一个虚函数表,当将构造函数声明为虚函数,在创建对象时,首先要调用构造函数,然后构造函数是虚函数,就需要用虚函数指针去调用,但是,对象都还没构造,也就没有虚函数指针,造成了一个矛盾的问题。

7.虚继承

  • 概念:
    C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

  • 解决问题:
    解决了二义性问题,也节省了内存,避免了数据不一致的问题。

  • 执行顺序:
    首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;
    执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
    执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;
    执行派生类自己的构造函数;
    析构以与构造相反的顺序执行;

8.简述一下虚函数和纯虚函数,以及实现原理

虚函数

基本介绍:

  • C++中的虚函数的作用主要是实现了多态的机制。
  • 如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数。
  • 虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。
  • 以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

实现原理:
虚函数表。简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。

虚函数表构造过程:

虚函数替换过程发生在编译时

  • 虚函数表创建时机
    虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚函数表创建时机是在编译期间。对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。
    编译期间编译器就为每个类确定好了对应的虚函数表里的内容。所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,所以,这个虚函数表指针就有值了。

  • 虚函数表指针(vfptr)创建时机
    vfptr跟着对象走,所以对象什么时候创建出来,vfptr就什么时候创建出来,也就是运行的时候。
    当程序在编译期间,编译器会为构造函数中增加为vfptr赋值的代码(这是编译器的行为),当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的vfptr赋值的语句。

纯虚函数

基本介绍:

  • 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。
  • 含有纯虚拟函数的类称为抽象类,它不能生成对象,用户不能创建类的实例
  • 必须在派生类中重新声明基类的纯虚函数(不要后面的=0)否则该派生类也不能实例化
  • 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。

原理:
类中含有纯虚函数时,其 vtable 不完全,有个空位。
即“纯虚函数在类的vtable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象”

C++中虚函数与纯虚函数的联系和区别

联系:

  1. 虚函数和纯虚函数可以定义在同一个类中,
  2. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
  3. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
  4. 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

区别:

  1. 含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类

  2. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

  3. 虚函数的定义形式:virtual{};
    纯虚函数的定义形式:virtual {} = 0;

9.抽象类的特点

  1. 抽象类只能用作其他类的基类,不能建立抽象类对象。

  2. 抽象类不能用作参数类型、函数返回类型或显式转换的类型。

  3. 可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

10.什么是多态?除了虚函数,还有什么方式能实现多态?

多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)

多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,

11.构造函数的隐式转换、显式转换

class Test 
{
public:
    Test(int n)
    {
        a = n;
    }
    int a;
};

int main()
{
    Test t = 1;
    cout << t.a << endl;
}

上面的代码中 Test t = 1;,编译器实际上是转换成了Test t = Test(1);。因为我们写了构造函数,编译器就按照我们的构造函数来进行隐式转换,直接把一个int数值隐式转换成了一个Test的对象。
但是,有些时候,我们不希望进行隐式转换,或者隐式转换会造成错误。比如:

class ClxString
{
public:
	ClxString(int iLength)
	{
		if (iLength > 0)
			m_pString = new char[iLength];
	}
	ClxString(const char* pString)
	{
		m_pString = new char[strlen(pString) + 1];
		strcpy_s(m_pString, strlen(pString) + 1, pString);
	}
	~ClxString()
	{
		if (m_pString != NULL)
			delete m_pString;
	}

public:
	char* m_pString;
};

int main()
{
	ClxString s1 = 13;  // 等同于ClxString lxTest = ClxString(13);

	ClxString s2 = "A";  // 等同于ClxString lxTest = ClxString("A");

	ClxString s3 = 'A';  // 等同于ClxString lxTest = ClxString(65);
}

如上所示的代码,前面两个ClxString s1 = 13;ClxString s2 = "A";都能按照我们想要的结果进行隐式转换,但是假如有人这么写ClxString s3 = 'A'就不一样了,A的ASCII编码为65,'A' 会被隐式转换为ClxString(65)。为了避免这种情况,可以将构造函数声明成explicit就可以防止隐式转换,如下:

explicit ClxString(int iLength)
{
    if (iLength > 0)
        m_pString = new char[iLength];
}

在这种情况下,要想用字符串的长度来初始化一个ClxString对象,那就必须显示的调用构造函数:

ClxString lxTest = ClxString(13);

而下面这些代码将不能通过编译:
ClxString lxTest = 13;
ClxString lxTest = 'A';

12.C++ 中哪些函数不能被声明为虚函数?

常见的不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

不能声明为虚函数的原因:

  1. 普通函数(非成员函数):因为普通函数(非成员函数)只能被重载,不能被重写。且编译器会在编译时绑定函数。
  2. 静态成员函数:静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。
  3. 内联成员函数:inline内联函数是在编译时被展开,虚函数是在运行时动态绑定。
  4. 构造函数:有虚函数就需要用到序表指针和虚函数表,虚表指针的初始化是在构造函数进行的,而虚函数需要放到虚表中。在调用虚函数前,必须首先知道虚表指针,但是构造函数还没有进行,互相矛盾了。
  5. 友元函数:C++不支持友元函数的继承,不能继承的函数指定不是虚函数。
posted @ 2022-10-08 15:52  小肉包i  阅读(33)  评论(0)    收藏  举报