类的六个默认成员函数->构造函数

在C++中当你创建一个空类时,编译器会默认声明一个default构造函数,copy构造函数,一个copy assignment操作符,一个析构函数。注意,编译器只声明,只有当这些函数被调用时,他们才会被创建。

  举个栗子,当你写下

class Empty() {};

  实际上在编译器中为你实现了这样的代码

  

class Empty
{
    Empty(){}
    Empty(const Empty& rhs){}
    ~Empty(){}
    Empty& operator=(const Empty& rhs){}
};

1.构造函数

1.默认构造函数

所谓默认构造函数,不仅仅是指编译器自己为你声明的构造函数,准确的说,默认构造函数应该是在创建类对象时不给对象传入任何参数的情况下,类对象调用的构造函数称为默认构造函数。

  那么默认构造函数分为两种,无参构造函数,带默认参数的构造函数(所有参数都有默认值)。举个栗子

class empty
{

};
class non_pat
{
public:
    non_pat(){}
};
class pat
{
public:
    pat(int _a = 0,double _b = 1.0)
    {
        a = _a;
        b = _b;
    }
private:
    int a;
    double b;

};
int main()
{
    empty e;
    non_pat n;
    pat p;//pat p(0,1.0);
    return 0;
}

在main函数中创建empty类对象时,调用编译默认创建的默认构造函数。non_pat和pat类对象时,都会调用本类的默认构造函数,pat对象没有传入参数,而我们手动实现的构造函数是有两个参数的,那他是否执行编译器默认创建的构造函数呢?不是的,只要我们手动声明了一个构造函数,编译器就不再为我们创建default构造函数。在pat类中,虽然没有无参构造函数,但是形参是有默认值的,当我们不传入参数时,实际上传入默认形参。可以认为我们实际上写下了了注释部分的代码。

2.构造函数做了什么?

  构造函数负责控制对象的初始化过程。实际上,构造函数做了两个阶段的工作。第一阶段为初始化阶段,负责为类对象分配空间并将类的数据成员初始化。第二阶段为计算阶段,负责调用构造函数函数体内的赋值操作,为类对象的数据成员赋值。我们通过代码来研究第一阶段,由于第二阶段执行函数体内的操作,所以我们先写一个构造函数函数体内误操作的类。

class Init
{
public:
    Init()
    {

    }
    int i;
    double d;
};
int main()
{
    Init init;
    std::cout << init.i << "," << init.d
                  << std::endl;
}

测试结果为618788,1.00043e-307.显然,没有赋值阶段,我们的类对象依然在内存中获得了空间,并且数据成员全部被赋予了随机值。但是显然我们的测试用例不够说服力,C++的内置类型使用随机值初始化,那如果数据成员本身就是一个类呢?我们再做测试

class pat
{
public:
    pat(int _a = 0,double _b = 1.0)
    {
        a = _a;
        b = _b;
    }int a;
    double b;

};
class Init
{
public:
    Init()
    {

    }
    int i;
    double d;
    pat p;
};
int main()
{
    Init init;
    std::cout << init.i << "," << init.d << std::endl;
    std::cout << init.p.a << "," << init.p.b << std::endl;
}

测试结果为

4259624,1.45552e-305

0,1

通过测试结果我们可知,当类的数据成员有类类型时,类类型的数据成员在初始化阶段执行自己的默认构造函数执行初始化,那如果这个类类型没有默认构造函数,该怎么办?我在后面会提到。

  接着我们谈到赋值阶段,在赋值阶段,执行构造函数体内的赋值操作。

  

class assign
{
public:
    assign()
    {
        i = 10;
        d = 10.00;
    }
    assign(int _i,double _d)
    {
        i = _i;
        d = _d;
    }
public:
    int i;
    double d;
};
int main()
{
    assign a;
    assign b(20,20.00);
    std::cout << a.i << "," << a.d << std::endl;
    std::cout << b.i << "," << b.d << std::endl;
}

测试结果为 10,10

20,20

结果与我们预想结果一致,没有什么可说的。

3.构造函数初始值列表

  前面我们提到,如果类的某个数据成员是类类型,那么在初始化阶段,执行这个数据成员类的默认构造函数。如果这个类没有默认构造函数,该怎么办?

  实际上,不只这种情况,如果类的成员函数时常量,也会出现问题,很简单,初始化阶段已经分配了随机值,那么在赋值阶段,我们可以给一个常量赋值吗?显然不能!!!

同理,成员函数是引用类型,也必须在初始化阶段就确定他的值。为了处理这三种情况,我们引入了初始化列表。

  形式如下:

  

int c = 20;
class A
{
public:
    A() : a(89),b(c){}
    const int a;
    int &b;
};
int main()
{
    A k;
    std::cout << k.a << "," << k.b << std::endl;
    return 0;
}

测试结果为:89,20

在初始化列表中,数据成员在初始化时,一方面在内存开辟空间,一方面直接用列表中的参数初始化数据成员 。其实可以用内置类型啦对比理解类的初始化阶段,计算阶段和初始化列表。

int a;//初始化阶段,开辟空间,使用随机值初始化
a = 10;//计算阶段,执行函数体内的操作,为数据成员赋值
int a = 10;//初始化列表的方式,在初始化时直接赋值。

理解了吗?我们再考虑一个问题,如果类的某个数据成员是类类型,那么在初始化阶段,执行他的默认构造函数,赋值阶段,执行他的赋值运算符重载函数。而如果他在初始化列表中初始化,则直接执行他的拷贝构造函数。我们来做一个测试。

class A
{
public:
    A()
    {
        std::cout << "constructions" << std::endl;
    }
    A(const A&rhs)
    {
        std::cout << "copy constructions" << std::endl;
    }
    A &operator =(const A&rhs)
    {
        std::cout << "assignment" << std::endl;
        return *this;
    }
};
A _a;
class B
{
public:
    B(){
       a = _a;
    }
public:
    A a;
};
class C
{
public:
    C() : a(_a){}
public:
    A a;
};
int main()
{
    std::cout << "main start" << std::endl;
    B b;
    C c;
    std::cout << "main end" << std::endl;
    return 0;
}

测试结果为constructors

main start

constructions

assignment

copy constructions

main end

分析一下,第三行的constructions表示B执行构造函数的初始化阶段,第四行赋值运算符重载表示赋值阶段,而C由于使用初始化列表,只执行一次拷贝构造。

总结如下:

1.类数据成员为常量

2.类数据成员为引用

3.类数据成员为没有默认构造函数的类类型。

在这三种情况下,构造函数必须使用初始化列表!!!即使无这三种情况中任意一种出现,也建议使用初始化列表来实现构造函数。因为初始化列表的方式将初始化和赋值结合在一步,省去了很多功夫,想象一下,如果某个数据成员为非常庞大的一个类,那么只执行一次他的拷贝构造函数要比执行一次他的默认构造函数加上赋值运算符重载函数要节省很多时间。

时间: 2024-10-19 09:30:55

类的六个默认成员函数->构造函数的相关文章

(继承及其访问限定符)&&(派生类及其默认成员函数)&&(赋值兼容规则)

◆继承: ★继承概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能.这样产生新的类,称派生类.继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程. 继承定义格式 ★继承关系&访问限定符 class Base { public: Base() { cout<<"B()" <<endl; } ~Base () { cout<<"~

(继承及其访问限定符)&amp;&amp;(派生类及其默认成员函数)&amp;&amp;(赋值兼容规则)

◆继承: ★继承概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能.这样产生新的类,称派生类.继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程. 继承定义格式 ★继承关系&访问限定符 class Base { public: Base() { cout<<"B()" <<endl; } ~Base () { cout<<"~

C++之中this指针与类的六个默认函数小结

我们先来看看this指针.之前看过一篇关于this指针的文章,觉得写的很好,今天决定自己来写一写,顺便总结一下C++里面关于类的一些内容. 什么是this指针呢?简单的说它是一个指向类的实例的指针,就好像当我们在进入一个房子之后,可以看见房子里的桌子,椅子.地板等, 但是看不到房子的全貌.对于一个类的实例来说,你可以看到它的成员函数.成员变量,但是实例本身呢?this是一个指针,它时时刻刻指向这个实例. 来看看this指针的特性: 1)this指针的类型是一个类类型 * const, 这表示什么

C++中的默认成员函数

一般而言,对于一个用户自定义的类类型,以下四个函数在用户没有自定义的情形下,会由编译器自动生成: 1.default constructor 2.copy constructor Someclass::Someclass(const Someclass &); 3.copy assignment operator Someclass & Someclass::operator=(const Someclass &); 4.destructor 对于C++11,又增加了两个: 5.m

如何禁止C++默认成员函数

如何禁止C++默认成员函数 发表于 2016-03-02   |   分类于 C++  |   阅读次数 17 前言 前几天在一次笔试过程中被问到C++如何设计禁止调用默认构造函数,当时简单的想法是直接将默认构造函数声明为private即可,这样的话对象的确不能直接调用.之后查阅了<Effective C++>之后得到了比较详尽的解释. 了解C++的默认行为 当我们创建空类时,C++默认给我们生成了四种成员函数: 构造函数 析构函数 拷贝构造函数(copy) 重载=的拷贝函数(copy ass

堆(stack) 之 c 和 c++模板实现(空类默认成员函数 初谈引用 内联函数)

//stack 的基本操作 #include <iostream> using namespace std; const int maxn = 3; typedef struct Stack { //NumType num; int num; }Stack; int top = 0;//当前元素位置的上一个元素 Stack stack[maxn]; bool is_empty(); bool is_full(); int pop(); void push(const int &key)

C++类的默认成员函数

成员函数隐含this指针参数: 每成员函数一个隐式的指针形参(构造函数除外): 对象在调用成员函数时,编译器会将对象的地址传递给this指针: 1.构造函数(需用一个公有成员函数对私有的成员变量进行初始化,在对象构造时执行一次,无参的构造函数和带缺省值的构造函数都是缺省构造函数 ,缺省的构造函数只能有一个) Mystring(const char *str="")//带缺省值的构造函数 { _str = new char[strlen(str) + 1]; strcpy(_str, s

C++6个默认成员函数

问题:C++中的空类,默认情况下会产生哪些类成员函数? 系统默认的缺省构造函数和拷贝构造函数(复制构造函数) 系统默认提供的析构函数. 系统默认的运算符重载函数(拷贝赋值函数):用于同类对象之间的赋值. 系统默认的取值运算:当对类的对象进行取地址(&)时会被调用. 1.构造函数法和析构函数存在的必要性和作用: (1).构造函数的必要性和作用(用构造函数确保初始化): 解决同种类型的不同对象的初始化问题. 保证每个对象的数据成员都有合适的初始值. (2).析构函数的必要性和作用(用析构函数确保清除

C#中Stack&amp;lt;T&amp;gt;类的使用及部分成员函数的源代码分析

Stack<T>类 Stack<T> 作为数组来实现. Stack<T> 的容量是 Stack<T> 能够包括的元素数. 当向 Stack<T> 中加入元素时,将通过又一次分配内部数组来依据须要自己主动增大容量. 可通过调用 TrimExcess 来降低容量. 假设 Count 小于堆栈的容量,则 Push 的运算复杂度是 O(1). 假设须要添加容量以容纳新元素,则 Push 的运算复杂度成为 O(n).当中 n 为 Count. Pop 的运