C++之类和对象

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之类和对象相关的知识,希望对你有一定的参考价值。

类和对象(三)


我们首先来看一个关于构造函数和析构函数调用顺序相关的一道题:

class A
{
public:
    A()
    {
        cout<<"A()"<<endl;
    }
    ~A()
    {
        cout<<"~A()"<<endl;
    }
    
};
class B
{
public:
    B()
    {
        cout<<"B()"<<endl;
    }
    ~B()
    {
        cout<<"~B()"<<endl;
    }
};
class C
{
public:
    C()
    {
        cout<<"C()"<<endl;
    }
    ~C()
    {
        cout<<"~C()"<<endl;
    }
};
class D
{
public:
    D()
    {
        cout<<"D()"<<endl;
    }
    ~D()
    {
        cout<<"~D()"<<endl;
    }
};

设已经有A,B,C,D,4个类的定义,程序中A,B,C,D析构函数调用顺序为?()

C c;
int main()
{
    A a;
    B b;
    static D d;
    return 0;
}

构造函数是定义对象的时候调用的,析构函数是在对象销毁的时候调用的,也就是生命周期结束的时候

栈要符合后进先出,在函数栈帧里面也要符合后进先出的性质,首先我们看a和b,a比b先定义,销毁的时候就是b先销毁,a再销毁,所以现在至少知道了b比a先调用析构函数,我们再来进一步分析,c是一个全局对象,d是一个静态对象,c和d对象在程序的整个运行期间都存在,直到main函数结束,c是在main函数之前就构造处理了, 静态的不是在main函数之前初始化的,而是在第一次调用这个函数时进行初始化的,然后再构造a,再构造b,再构造d,那么析构顺序是怎么样的呢?析构顺序是后定义的先析构,因为c是再main函数进去之前就定义了,所以它是在最后析构,那么a、b、d是怎么样的顺序呢?首先a和b的顺序是b先析构,然后a再析构,那么d是在什么时候析构呢?后定义的不是先析构吗?那么顺序是d、b、a吗?不是的,因为d是静态变量,他是在第一次调用当前域函数的时候初始化,它是静态的,当第二次调用函数的时候就不进行初始化了,它是存在静态区的,它和c一样都是在main函数结束后才销毁的,那么d为什么先比c销毁,是因为它们都在静态区,先定义的后析构,所以析构函数的调用顺序是:b,a,d,c。

再谈构造函数

构造函数体赋值

在创建一个对象时,编译器通过调用构造函数给对象的各个成员进行初始化

此时我们有一个难题:

class A
{
public:
    A(int a)
    {
        _a=a;
    }
private:
    int_a;
}
class B
{
private:
    int _b;//内置类型
    A _aa;//自定义类型
};
int main()
{
    B b;
}

B类里面有一个内置类型和一个自定义类型的成员变量,我们现在要对它们初始化,假设B类我们不写构造函数,编译器会生成默认构造函数,对自定义类型会调用自定义类型的构造函数,对内置类型不处理。当我们不写B类的构造函数,给A类写一个带参的构造函数时,编译器会报错:

这里本质上是B生成的默认构造函数去调用A的默认构造函数,但是A没有默认构造函数,所以会报错。

我们将A类的构造函数改为默认构造函数就解决了:

对于编译器默认生成的默认构造函数内置类型不处理,自定义类型处理,C++11打了一个补丁:

class B
{
private:
    int _b = 1;//内置类型
    A _aa;//自定义类型
};

这里是声明,而不是初始化,后面给一个缺省值,当没有处理_b时,就会用默认的,也就是1

但是这样比较尴尬,这里的_b只能初始化为1,那么我们写一个带参的构造函数

B(int a,int b)
{
    
}

那么这里怎么初始化呢?感觉不好初始化,只要_b好初始化

B(int a,int b)
{
    _b = b;
}

关键是_aa怎么初始化呢?这样吗?

B(int a,int b)
{
    _aa._a = a;
    _b = b;
}

这样是不行的,因为_a在A类是私有的,是不可以访问或者修改的

那么要怎么处理这个问题呢?这样就可以初始化:

B(int a,int b)
{
   	A aa(a);
    _aa = aa;
    _b = b;
}

这里定义了A类型aa对象,调用了构造函数初始化了aa对象,再将aa对象赋值给_aa,这里又调用了赋值重载函数,这里是比较麻烦的。

实际上我们也可以定义一个匿名对象进行:

B(int a,int b)
{
   	_aa = A(a);//匿名对象,只是想临时用用
    _b = b;
}

匿名对象的生命周期是定义的这一行

所以我们确定的初始化的解决为:

class A
{
public:
	A(int a = 0)
	{
		_a = a;
		cout << "A(int a = 0)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "operator=(const A& a)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
class B
{
public:
	B(int a, int b)
	{
		_aa = A(a);//匿名对象,只是想临时用用
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10,20);
	return 0;
}

可以看到调用了A类的两次构造函数,一次赋值重载函数

可能有的人会疑惑,为什么会有两个构造?

因为在b对象被定义出来以后,有一个自定义类型,有一个内置类型,在B类的构造函数里面它会默认先去调用A的构造:

我们可以通过反汇编可以看到在B的构造函数里面调用了A的构造函数。

我们这样写了一个构造函数完成了初始化,但是可以看到非常麻烦,要初始化这个_aa代价很大,这个是函数体内初始化,接下来我们来看初始化列表初始化:

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

对于没有自定义类型成员时函数体内初始化和使用初始化列表初始化没有什么区别,而在有自定义类型成员时,函数体内初始化改用初始化列表可以提高效率。

因为在有自定义类型成员时,函数体内初始化付出的代价高,而初始化列表初始化呢?

class A
{
public:
	A(int a = 0)
	{
		_a = a;
		cout << "A(int a = 0)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "operator=(const A& a)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
class B
{
public:
	B(int a, int b)
        :_aa(a),_b(b)
	{
		//_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10,20);
	return 0;
}

使用初始化列表初始化只调用了一个构造函数就完成了初始化,不写初始化列表,我们在函数体开始时也要调用构造,只是调用的是默认构造,如果显式的去写,就可以去调用带参的构造

  • 尽量使用初始化列表初始化

因为就算不写,编译器也会认为是有初始化列表的_b,_aa会使用默认初始化列表进行初始化,可以说初始化列表是成员变量定义的地方,所以建议能使用初始化列表初始化的成员尽量使用初始化列表初始化

class B
{
public:
	B(int a, int b)
	{
		_aa = A(a);//匿名对象,只是想临时用用
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10,20);
	return 0;
}

尽管我们没有显式的写初始化列表,那么这里也是认为有初始化列表的,_b和_aa会使用默认初始化列表进行初始化,我们也可以认为初始化列表是对象的成员变量定义的地方

我们调式可以看到还没有走函数体内的初始化,他已经被初始化了,所以一般的建议是:能使用初始化列表初始化的成员尽量使用初始化列表进行初始化

  • 有些成员必须使用初始化列表初始化

1.const修饰的变量必须使用初始化列表初始化

class B
{
public:
    B(int c)
    {
        _c=c;
    }
    private:
    const int _c;
    int& _ref;
};
int main()
{
    B b(1);
    return 0;
}

使用函数体内初始化它会报错,为什么呢?

const修饰的变量必须在定义的时候初始化,那么对象的成员在初始化列表定义,已经定义过了就不能再在函数体内改了

2.引用也必须使用初始化列表进行初始化

class B
{
public:
    B(int c,int& ref)
        :_c(c)
    {
        _ref=ref;
    }
private:
    const int _c;
    int& _ref;
};
int main()
{
    int ref = 10;
    B b(1,ref);
    return 0;
}

为什么呢?

是因为引用也必须在定义的时候初始化

3.自定义类型成员(该类没有默认构造函数)

class A
{
public:
	A(int a)
	{
		_a = a;
		cout << "A(int a = 0)" << endl;
	}
private:
	int _a;
};
class B
{
public:
    B(int c,int& ref)
        :_c(c),_ref(ref)
    {
    }
private:
    const int _c;
    int& _ref;
    A _aa;
};
int main()
{
    int ref = 10;
    B b(1,ref);
    return 0;
}

这里会报错,因为不写,它会调用默认的构造函数,写了就会调用带参的构造函数

总结:

  1. 尽量使用初始化列表初始化,因为就算你不显示用初始化列表,成员也会先用初始化列表初始化一遍。

  2. 有些成员是必须在初始化列表初始化的。引用、const、没有默认成员函数、的成员

  3. 初始化列表和函数体内初始化,可以混着用,互相配合

比如实现带头双向循环链表

//带头双向循环链表
List(): _head(BuyNode(0))
{
    //有些初始化用初始化列表完成不了,还得函数体内初始化,所以我们要注意灵活应用
    _head->next = _head;
    _head->prev = _head;
}
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
    A(int a)
    :_a1(a)
    ,_a2(_a1)
	{}
void Print() 
{
	cout<<_a1<<" "<<_a2<<endl;
}
private:
    int _a2;
    int _a1;
};
int main() 
{
    A aa(1);
    aa.Print();
}

上面这段程序会输出什么呢?

A. 输出1 1

B.程序崩溃

C.编译不通过

D.输出1 随机值

答案是D,为什么呢?

因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,这里是先初始化a2,a2是用a1进行初始化的,但是此时a1还没初始化,a1是随机值,所以a2是随机值,然后再用a初始化a1,故a1为1,所以最后输出1和随机值,在实际中建议声明顺序和初始化列表的顺序保持一致,避免出现这样的问题

explicit关键字

我们首先看下面这个代码:

class A
{
public:
    A(int a):_a(a)
    {}
private:
    int _a;
};
int main()
{
    A aa1(1);
    return 0;
}

这里就是创建了aa1对象,调用了它的构造函数

还可以这样写:

class A
{
public:
    A(int a):_a(a)
    {}
private:
    int _a;
};
int main()
{
    A aa1(1);
    //下面本质是一个隐式类型转换
    A aa2 = 2;
    return 0;
}

第11行是调用构造函数,那么第13行是干什么呢?

它其实是隐式类型转换,int转换成A了,为什么int能转换成A呢?因为它有一个用int去初始化A的构造函数,有这个构造函数就支持,没有这个构造函数就不支持

A aa2 = 2;

怎么证明这一点呢?我们打印出来:

可以看到调用了两次构造函数,并没有调用拷贝构造

232行和234行虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程是不一样的,234行是优化后的结果

如果你不想这种隐式类型转换的发生,你可以在构造函数前面加个关键字explicit:

class A
{
public:
    explicit A(int a) :_a(a)
    {
        cout << "A(int a)" << endl;
    }
    A(const A& a)
    {
        cout << "A(const A& a)" << endl;
    }
private:
    int _a;
};
int main()
{
    A aa1(1);
    //下面本质是一个隐式类型转换
    A aa2 = 2;
    return 0;
}

此时就会报错无法从int转换为A

上面是一个参数的情况,那么如果是多个参数呢?

class A
{
public:
    explicit A(int a,int b) :_a(a)
    {
        cout << "A(int a)" << endl;
    }
    A(const A& a)
    {
        cout << "A(const A& a)" << endl;
    }
private:
    int _a;
};
int main()
{
    A aa1(1,2);
    return 0;
}

如果是直接调用构造函数,就传多个参数就可以了,那么第二种怎么写呢?

在C++98中是不支持多参数的,但是在C++11中支持:

int main()
{
    A aa2 = {12};
    return 0;
}

C++11支持多个参数时这样写,道理是一样的,编译器会进行优化成只是一个构造

同样地,你不想发生隐式类型转换,你也可以加关键字explicit

static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化

我们来看一个面试题:实现一个类,计算程序构造了多少个对象。(构造+拷贝构造)

int countn = 0;
class A
{
public:
    A()
    {
        ++countn;
    }
    A(const A& a)
    {
        ++countn;
    }
};
A f(A a)
{
    A ret(a);
    return ret;
}
int main()
{
    A a1 = f(A());
    A a2;
    A a3;
    a3 = f(a2);
    cout<<countn<<endl;
    return 0;
}

可以看到有8个

这样写有一个问题,万一别人写时,把你的countn改了呢?

有什么好的方式让别人不能轻易的改呢?

C++当中有静态变量,成员变量不仅可以是普通的变量,也可以是静态变量

class A
{
public:
    A()
    {
        ++_count;
    }
    A(const A& a)
    {
        ++_count;
    }
private:
    //声明
    int _a;
    static int _count;
};
//定义初始化
int A::_count = 0;
A f(A a)
{
    A ret(a);
    return ret;
}

int main()
{
    A a1 = f(A());
    A a2;
    A a3;
    a3 = f(a2);
    return 0;
}

_a和_count有什么区别呢?

_a存储在定义出的对象中,属于某个对象,而_count存在静态区,属于整个类,也属于每个定义出来的对象共享,跟全局变量比较,他受类域和访问限定符限制,更好体现封装,别人不能轻易修改

注意_count没有存在对象里面

int main()
{
    A a1 = f(A());
    A  a2;
    A a3;
    a3 = f(a2);
    cout << sizeof(A) <<endl;//算的是A定义出来的对象的大小
    return 0;
}

可以看到sizeof(A)是4,并没有算_count。

需要注意的是:

静态成员变量不能在构造函数初始化,它在全局位置定义初始化,并且它是私有的,是不能改的

int main()
{
    A a1 = f(A());
    A  a2;
    A a3;
    a3 = f(a2);
    A::_count = 10;//我们假设要修改,会报错
    return 0;
}

那么我们要访问_count需要怎么访问呢?这样吗?

cout <<A::_count <

以上是关于C++之类和对象的主要内容,如果未能解决你的问题,请参考以下文章

C++之类和对象的特性

C++之类和对象

C++之类和对象的使用

C++之类和对象

C++基础之类和对象二

C++之类和对象的使用