C++类与对象(一)—类定义、构造函数、struct与class的区别

类定义

类是C++的核心特性,通常被称为用户定义的类型。类用于指定对象的形式,它包含数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员,函数在一个类中被称为类的成员。

C++ primer plus中的解释:我们来看看是什么构成了类型。首先,倾向于根据数据的外观(在内存中如何存储)来考虑数据类型。例如,char占用1个字节的内存,而double通常占用8个字节的内存。但是稍加思索就会发现,也可以根据要对它执行的操作来定义数据类型。例如,int类型可以使用所有的算术运算,可以对整数进行加减乘除运算,还可以求模;而指针需要的内存数量很可能与int相同,甚至可能在内部被表示为整数,但不能对指针执行与整数相同的运算,例如,不能将两个指针相乘。因此,将变量声明为int或者float指针的时候,不仅仅是分配内存,还规定了可对变量执行的操作。总之,指定基本类型完成了三项工作:1、决定了数据对象需要的内存数量;2、决定如何解释内存中的位(long和float在内存中占用的位数相同,但是将题目转换为熟知的方法不同);3、决定可使用数据对象执行的操作或方法
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包

构造函数

参考C++中的五种构造函数
构造函数是一种特殊的函数,用来在对象实例化的时候初始化对象的成员变量。C++有五种构造函数

默认构造函数

参考什么是构造函数?
未提供显示初始值的时候,是用默认构造函数。包括以下两种情况

1、没有带明显形参的构造函数
2、提供了默认实参的构造函数

类只含有内置类型或者复合类型的成员的时候,编译器不会为类合成默认构造函数;默认构造函数“被需要”(对于编译器)的时候,编译器才会合成默认构造函数。

何时默认构造函数才会被编译器需要?

含有类对象数据成员,该类对象有默认构造函数
class A
{
    
    
public:
    A(bool _isTrue=true, int _num = 0){
    
     isTrue = _isTrue; num = _num; }; //默认构造函数
    bool isTrue;
    int num;

};
class B
{
    
    
public:
    A a;//类A含有默认构造函数
    int b;
    //...
};
int main()
{
    
    
    B b;    //编译至此时,编译器将为B合成默认构造函数
    return 0;
}
基类带有默认构造函数派生类

一个类派生自一个含有默认构造函数的基类的时候,该类也是“被需要”的。==如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去。

带有虚函数的类

这可以分为两种情况:1、类本身定义了自己的虚函数 2、类从继承体系中继承了虚函数(成员函数一旦被声明为虚函数,继承不会改变虚函数的性质)

这两种情况都使一个类成为带有虚函数的类。这样的类也满足编译器需要合成默认构造函数的类,原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。

带有虚基类的类

如果A虚继承与类X,对于A来说,X就是A的虚基类。虚基类是为了解决多重继承下确保子类对象中每个父类只含有一个副本的问题,比如菱形继承:在编译阶段无法却低估是哪个虚基类对象,所以编译器会产生一个指向虚基类的指针,这个指针的安插,编译器将会在合成默认构造函数中完成

普通构造函数

C++用于构建类的新对象的时候调用的函数

拷贝构造函数

当一个类没有拷贝构造函数的时候,如果满足以下四个条件之一,编译器会为该类自动生成一个默认的拷贝构造函数(浅拷贝

  1. 该类含有一个类类型(不是内置类型)的成员变量,并且这个类型含有拷贝构造函数
  2. 该类继承自含有拷贝构造函数的类
  3. 该类声明或者继承了虚函数
  4. 该类含有虚基类

深拷贝和浅拷贝

取决于对象中的数据成员是否含有指针。

  • 如果不含有指针类型,那么是浅拷贝
  • 如果含有指针,那么是深拷贝,赋值对象中每个数据成员的值的时候,对于指向动态分配内存的指针类型数据成员,需要重新分配内存空间,并且将值复制过去。这时因为指针类型数据成员所指向的内存空间可能在对象析构的时候被释放,然后造成错误。

拷贝函数调用的时机

用一个对象去初始化同类的另一个对象
//这两条语句是等价的。第二条是初始化语句,不是赋值语句。赋值语句不会引发复制构造函数的调用
A c2(c1);
A c2=c1;
作为形参的类的对象,是用复制构造函数初始化的。

而且调用复制构造函数时的参数也就是调用函数时所给的实参。

#include<iostream>
using namespace std;
class A{
    
    
public:
    A(){
    
    };
    A(A & a){
    
    
        cout<<"Copy constructor called"<<endl;
    }
};
void Func(A a){
    
     }
int main(){
    
    
    A a;
    Func(a);//Copy constructor called

    return 0;
}

作为函数返回值的对象是用复制构造函数初始化的

此时虽然发生named return value优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数。C++编译器发生NRV优化,如果是引用返回就不会调用拷贝构造函数,如果是传递的方式依旧会发生拷贝构造函数。

==理论上的执行过程是:产生临时对象、调用拷贝构造函数把返回对象拷贝给临时对象、函数执行完先析构局部变量,再析构临时对象、依然会调用拷贝构造函数。

Linux g++不会发生拷贝构造,即使是返回局部对象的引用,也不会发生拷贝构造

#include<iostream>
using namespace std;
class A {
    
    
public:
    int v;
    A(int n) {
    
     v = n; };
    A(const A & a) {
    
    
        v = a.v;
        cout << "Copy constructor called" << endl;
    }
};
A Func() {
    
    
    A a(4);
    return a;
}
int main() {
    
    
    cout << Func().v << endl;
    return 0;
}

输出结果:

Copy constructor called
4

转换构造函数

一个构造函数接收一个不同于其类型的形参,可以视为将其形参转换成类的一个对象。比如C++将C字符串转换成string。

移动构造函数

对于程序执行过程中产生的临时对象,往往只用于传递数据,并且会很快销毁。因此在使用临时对象初始化新对象的时候,可以将其包含的指针成员指向的内存资源直接移给新对象所有,不需要再拷贝对象。

什么时候触发移动?

临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候就可以触发移动构造

移动构造函数是深拷贝还是浅拷贝

  • 若对象中的数据成员不含指针类型,那么移动构造函数进行的是浅拷贝,即仅仅复制对象中的每个数据成员的值,不涉及任何内存分配操作。
  • 若对象中的数据成员含指针类型,那么移动构造函数进行的是深拷贝和浅拷贝都可能存在。具体来说,如果移动构造函数采用的是转移指针的方式,将移动源中的指针成员转移到移动目标中,那么就是浅拷贝。如果移动构造函数采用的是重新分配内存空间的方式,将移动源中的指针成员指向的内存重新分配到移动目标中,那么就是深拷贝。需要注意的是,在实现移动构造函数时应该尽量避免分配内存或者进行任何昂贵的深拷贝操作,以保证移动构造函数的高效性。
采用浅层复制的时候,如何避免“第一个指针释放空间而导致第二个指针指向不合法?

避免第一个指针释放空间。==将第一个指针置为NULL,调用析构函数 的时候,由于有判断是否为NULL的语句,析构的时候就不会回收空间。

委托构造函数

委托构造函数是C++11新特性。使用当前类的其它构造函数来帮助当前构造函数初始化。换句话来说,就是可以将当前构造函数的部分或者全部职责交给本类的另一个构造函数。

构造函数和析构函数

构造函数是否可以声明为虚函数或者纯虚函数?析构函数呢?

构造函数

存储空间

虚函数对应一个指向虚表的指针,这个指向虚表的指针事实上是存储在对象的内存空间的。如果构造函数是虚函数,就要通过虚表来调用,但是这个时候对象还没有实例化,没有这个内存空间,找不到虚表。

使用角度

虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数,而构造函数是在创建对象的时候自己主动调用的,不可能通过父类的指针去引用或者调用,所以构造函数不能是虚函数。

实际含义上看

构造函数作用是初始化,在对象生命期间仅仅运行一次,不是对象的动态行为,没必要成为虚函数。

析构函数

析构函数一般写成虚函数。由于类的多态性,基类指针可以指向派生类的对象,如果删除这个基类的指针,就会调用这个指针指向的派生类的析构函数,派生类的析构函数有自动调用基类的虚构函数(编译器规定的)这样整个派生类的对象完全被释放。

如果析构函数不被声明为虚函数,则编译器实施静态绑定,在删除基类指针的时候,只会调用基类的析构函数而不会调用派生类的析构函数,这样派生类就会析构不完全,造成内存泄漏。(我们往往通过基类的指针来销毁对象,这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数)
总而言之就是防止只析构基类而不析构派生类从而内存泄漏

存在一种特例:在CRTP模板中,不应该将析构函数声明为虚函数,理论上所有父类函数都不应该声明为虚函数,因为这种继承方式不需要虚函数表

纯虚析构函数

纯虚析构函数一定要定义,否则如果某个派生类没有提供自己的析构函数实现,就会导致链接失败。这是因为在派生类析构函数中需要调用其基类的析构函数,但如果基类的析构函数是纯虚函数且未被定义,那么编译器无法进行链接,从而导致编译错误。

因此,建议不要将析构函数声明为纯虚函数,而应该提供默认实现,以便派生类可以继承并实现自己的析构函数。在需要实现多态性但又不需要提供默认实现的情况下,可以将析构函数声明为虚函数,不需要加上纯虚函数的关键字。这样可以保证在其它地方调用派生类的析构函数时不会出现问题,同时也可以提供一个默认实现可以避免编译错误。

构造函数、析构函数、虚函数可否声明为内联函数

构造函数和析构函数

在语法上没有错误,但是没有意义的,因为编译器不会真正对声明为inline的构造和析构函数进行内联操作(因为编译器会在构造和析构函数中添加额外的操作:申请/释放内存,构造/析构对象等,致使构造函数或者析构函数并不像看上去那么精简;其次,class中的函数默认是inline型的,编译器也是有选择地inline,将构造函数和析构函数声明为内联函数是没有什么意义的

虚函数

要分情况。

  • 当指向派生类的指针(多态性)调用声明为inline的虚函数的时候,不会内联展开
  • 当是对象本身调用虚函数的时候,会内联展开

这是因为指向派生类的指针调用虚函数时需要通过虚函数表来确定要调用的函数,这个过程不能在编译时确定,因此不能内联展开。而对象本身调用虚函数时,编译器已经知道要调用的函数地址,可以在编译时将函数代码插入到调用处,实现内联展开。

析构函数如何起作用

析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。撤销对象的时候,编译器也会自动调用析构函数。
一般析构函数定义为类的公有成员。

构造函数和析构函数可以调用虚函数吗

  • 在构造函数期间,对象的虚函数表尚未建立,因此无法进行虚函数调用。在构造函数执行期间,对象只是处于部分构造状态,对象的派生类部分的成员尚未初始化,如果这时候调用派生类重写的基类的虚函数,则无法进行正确的虚函数绑定,导致出现错误。C++不会动态联编,运行的是构造函数自身类型定义的版本解决这个问题的方法是,可以在基类构造函数中显式调用非虚函数或静态函数,以确保不会进行虚函数绑定。

  • 在析构函数期间,对象的虚函数表已经被销毁,无法进行虚函数调用。如果在析构函数中调用虚函数,将会调用到基类的虚函数,而不是派生类的虚函数,因为此时虚函数表已经被销毁,派生类的虚函数表已不可用。解决这个问题的方法是,可以将析构函数声明为虚函数,以保证析构函数能够正确地销毁对象,按照继承关系从派生类到基类顺序执行析构函数。

构造函数和析构函数顺序

构造函数

  • 基类构造函数。如果有多个基类,构造函数调用顺序是某类在类派生表中出现的顺序,而不是他们出现在成员初始化表中的顺序。
  • 成员类对象构造函数。如果有多个成员类对象则构造函数调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  • 派生类构造函数。

类派生表(Class Derivation Table)是一种数据结构,用于描述一个类的继承关系和派生类中新增的数据成员和虚函数等信息。在C++中,类的继承关系一般由编译器自动产生并管理,其中类派生表是一个非常重要的结构。
在类的内存布局中,类派生表一般位于类对象的最前面,包含了一组指针,每个指针指向一个基类的类派生表或虚函数表,从而组成一张继承图。在类的构造函数和析构函数等操作中,编译器会借助类派生表来实现正确的对象初始化和内存释放。
类派生表中包含的信息与具体编译器的实现方式有关,不同编译器的类派生表可能会有差异。一般来说,类派生表包含了如下信息:
1.基类列表,指明了派生类继承的基类以及继承方式(公有继承、私有继承或保护继承)。
2.新增数据成员的偏移量,指示了派生类中新增的成员相对于对象起始地址的偏移量。
3.派生类中新增虚函数指针的偏移量,指示了派生类中新增的虚函数对象相对于对象起始地址的偏移量,并将其与虚函数表中对应基类的虚函数指针对应起来。
4.虚基类表偏移量,指示了虚基类表相对于对象起始地址的偏移量,用于处理多重继承中的虚基类情况。
在运行时,类派生表的信息将用于完成对象的构造和析构、父类和子类间的转换、继承的实现和运行时类型识别等操作。

析构函数

  • 派生类
  • 成员类对象的析构函数
  • 基类

struct和class区别

区别

相同

  • 两者都拥有成员函数、public和private部分
  • 任何可以使用class完成的工作,同样可以使用struct完成。

不同

  • struct 队成员是默认public的,而class默认是private的
  • class默认是私有继承,而struct默认是公有继承

引申:C++和C的struct区别

  • C中struct是用户自定义数据类型;C++中式抽象数据类型,可以支持成员函数的定义(C++中的struct可以继承,可以实现多态)
  • C中的结构体没有权限设置,只能是一些变量的结合体,可以封装数据但是不能隐藏数据,成员不可以是函数
  • C++中,struct增加了访问权限,而且可以和类一样有成员函数
  • 写法不一样。
  • C和C++中struct写法
//************************************************
//C语言中,定义一个struct需要加上关键字struct
struct student {
    
    
    int id;
    char name[20];
};
//************************************************
//C++中,可以省略关键字struct
struct student {
    
    
    int id;
    char name[20];
};

// 可以简写成
struct student {
    
    
    int id;
    char name[20];
};


此外,在使用struct的时候

//C语言要加上struct关键字
struct student stu;

//C++可以省略
student stu;

struct变量比较是否相等

如果是元素,一个个比;指针直接比较,如果保存的是同一个实例地址,则p1==p2为真

struct foo {
    
    

  int a;
  int b;

  bool operator==(const foo& rhs) *//* *操作运算符重载*

  {
    
    
    return( a == rhs.a) && (b == rhs.b);
  }
};

猜你喜欢

转载自blog.csdn.net/qaaaaaaz/article/details/130458524
今日推荐