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

本文详细介绍了C++中的类,包括构造函数的五种类型(默认构造函数、拷贝构造函数、移动构造函数等),以及构造函数和析构函数的作用、调用顺序和规则。文章还探讨了构造函数和析构函数是否可以是虚函数,以及内联函数、深拷贝和浅拷贝的概念。此外,提到了struct与class的区别以及C++中的类型定义和成员函数。
摘要由CSDN通过智能技术生成

类定义

类是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

构造函数、拷贝构造函数和赋值操作符的区别

  • 构造函数:对象不存在,没用别的对象初始化,创建一个新的对象时调用构造函数
  • 拷贝构造函数:对象不存在,但是使用别的已经存在的对象来进行初始化。产生新的类对象,在初始化对象之前不需要检查源对象和新建对象是否相同
  • 赋值运算符:对象存在,用别的对象给他赋值。这属于重载等号的范畴。赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉

类中有指针变量的时候要重写析构函数、拷贝构造函数和赋值运算符。

为什么拷贝构造函数必须传递引用而不能传值?

如果用值传递,构造实参需要调用拷贝构造函数而拷贝构造函数(初始化形参)需要传递实参。

Effective C++ 条款20:宁以const 引用传递替换值传递:
1、引用更高效
2、 避免切割问题出现
3、但是以上规则不适合内置以及STL迭代器和函数对象,对于他们来说值传递更合适。对于内置类型,值传递更加高效;对于STL迭代器和函数对象,习惯上是被设计为值传递。

如何禁止程序自动生成拷贝构造函数

  • 为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;

  • 解决办法:定义一个基类,基类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类编译器不会自动生成这两个函数。

转换构造函数

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

移动构造函数

对于程序执行过程中产生的临时对象,往往只用于传递数据,并且会很快销毁。因此在使用临时对象初始化新对象的时候,可以将其包含的指针成员指向的内存资源直接移给新对象所有,不需要再拷贝对象。
移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在源地址上进行新对象的构造,最后调用原对象的析构函数,这样就不会给新对象分配空间

Example6 (Example6&& x) : ptr(x.ptr) 
  {
    x.ptr = nullptr;
  }

  // move assignment
  Example6& operator= (Example6&& x) 
  {
   delete ptr; 
   ptr = x.ptr;
   x.ptr=nullptr;
    return *this;
}

什么时候触发移动?

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

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

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

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

委托构造函数

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

构造函数和析构函数

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

构造函数

存储空间

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

使用角度

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

实际含义上看

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

析构函数

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

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

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

纯虚析构函数

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

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

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

构造函数和析构函数

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

虚函数

要分情况。

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

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

析构函数如何起作用

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

类什么时候会析构

  • 对象生命周期结束的时候,被销毁时。
  • delete指向对象的指针,或delete指向对象的基类指针的时候,而其基类析构函数是虚函数的时候
  • 如果对象A是对象B的成员,B的析构函数被调用,那么A的析构函数也被调用

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

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

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

构造函数和析构函数顺序

构造函数

  • 基类构造函数。如果有多个基类,构造函数调用顺序是某类在类派生表中出现的顺序,而不是他们出现在成员初始化表中的顺序。
  • 对象的vptr被初始化
  • 成员类对象构造函数。如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr设定之后才能做。如果有多个成员类对象则构造函数调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  • 派生类构造函数。
  • 执行程序员写的代码。

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

一个类中的全部构造函数的扩展过程是什么?
  • 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序。
  • 如果一个成员没有出现在成员初始化列表中,但是它有一个默认构造函数,那么默认构造函数必须被调用
  • 如果class有虚表,那么它必须被设定初值
  • 所有上一层的基类构造函数必须被调用
  • 所有虚基类构造函数必须被调用

析构函数

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

构造函数和析构函数可否抛出异常

  • C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。==因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用,因此造成内存泄漏。
  • 用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;
  • 如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;

构造函数

构造函数可以抛出异常,把异常抛出到调用方,调用方根据需要捕获异常并进行适当处理。

注意,如果在构造函数中使用异常抛出,则必须在类声明中使用异常说明符来声明构造函数可能抛出的异常。如果没有指定异常,调用方将无法处理可能的异常,从而导致程序终止。

析构函数

如果在析构函数抛出异常,根据effective C++条款 08:别让异常逃离析构函数。因为有可能导致不明确行为。

有两个办法避免这个问题:

1、 抛出异常的时候结束程序,通常通过调用abort完成(阻止异常从析构函数传播出去)
2、把异常捕捉,虽然这也不是一个好主意,因为压制了“某些动作失败的重要信息”,但是有时候也比“草率结束程序”或者“不明确行为带来的风险”要好。

一个更好的策略

重新设计接口,让客户有机会对可能出现的问题作出反应。把调用一个函数的责任从析构函数转移到客户手上。如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数)中执行操作

构造函数的几种关键字

default

显式要求编译器生成合成构造函数,防止在调用时相关构造函数没有定义而出错。

delete

删除构造函数、赋值运算符等。

0

虚函数定义为纯虚函数,纯虚函数无需定义。当然也可以为纯虚函数提供定义,函数体可以定义在类的外部也可以定义在内部

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);
  }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值