一、构造函数与析构函数
构造函数是特殊的成员函数
创建类类型的新对象,系统自动会调用构造函数
构造函数是为了保证对象的每个数据成员都被正确初始化
几点详细说明:
函数名和类名完全相同
不能定义构造函数的类型(返回类型),也不能使用void
通常情况下构造函数应声明为公有函数,否则它不能像其他成员函数那样被显式地调用
构造函数被声明为私有有特殊的用途。
构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数(重载)
一个由c/c++编译的程序占用的内存分为以下几个部分
1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表, 3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区―存放函数体的二进制代码。
不带参数的构造函数
如果程序中未声明,则系统自动产生出一个默认构造函数
全局对象的构造先于main函数
析构函数可以显示调用(很少这么做 只在一些特殊的场合才这这样)
test.h
#ifndef _TEST_H_
#define _TEST_H_
class Test
{
public:
Test();
Test(int num);
void Display();
~Test();
private:
int num_;
};
#endif
test.cpp
#include"Test.h"
#include<iostream>
using namespace std;
Test::Test()
{
num_ = 0;
cout<<"initializeing Default"<<endl;
}
Test::Test(int num)
{
num_ = num;
cout<<"initializeing..."<<num_<<endl;
}
void Test::Display()
{
cout<<num_<<endl;
}
Test::~Test()
{
cout<<"Distroy"<<num_<<endl;
}
main.cpp
#include "Test.h"
#include <iostream>
using namespace std;
//int main(void)
//{
// Test t;
// t.Display();
//
// Test t2(10);
// t2.Display();
//
// //不仅仅分配了内存 还调用了构造函数
// Test *t3 = new Test(20);
// t3->Display();
//
// //不仅仅释放了内存 还调用了析构函数 并且析构函数不能够被重载,不带参数
// //析构与构造的顺序相反
// //t3是堆内存,必须由程序员自己释放
// //t1与t2是栈内存,当花括号结束后,再释放对象
// delete t3;
//
// return 0;
//}
//全局对象会比Main函数先于构造
//
//Test t(10);
//int main()
//{
// cout<<"Entering main"<<endl;
// cout<<"Exiting main"<<endl;
//}
//析构函数与数组
//int main(void)
//{
// Test t[2] = {10,20};
// Test *t2 = new Test(2);
// delete t2;
//
// Test *t3 = new Test[2];
// //必须要有[] 否则会出现运行时错误
// delete[] t3;
// return 0;
//}
//析构函数可以显示调用
int main(void)
{
Test t;
//会有风险 如果析构函数里面释放内存的话 这样会释放两次
//因为Test对象在生命周期结束时候,会自动调用析构函数
t.~Test();
return 0;
}
二、转换构造函数、赋值与初始化区别、explicit
转换构造函数:单个参数的构造函数
作用:将其它类型转换为类类型
所以构造函数的作用:
1、初始化(普通构造函数的功能) 2、类型转化(转换构造函数)
#include "Test.h"
int main(void)
{
Test t(10); // 带一个参数的构造函数,充当的是普通构造函数的功能
t = 20; // 将20这个整数赋值给t对象
// 1、调用转换构造函数将20这个整数转换成类类型 (生成一个临时对象)
// 2、将临时对象赋值给t对象(调用的是=运算符 将调用默认的=运算符 如同系统提供默认构造函数)
Test t2;
}
上面程序的输出为:
临时对象赋值成功后直接销毁。可以看出:类的构造函数只有一个参数是非常危险的,
因为编译器可以使用这种构造函数把参数的类型隐式转换为类类型
赋值与初始化区别:
Test t = 10; // 等价于Test t(10);这里的=不是运算符,表示初始化。
注:是普通的构造函数 不会产生临时对象 表示初始化
Test& operator=(const Test& other); 系统默认做的操作是成员赋值
Test& Test::operator=(const Test& other)
{
cout<<"Test::operator="<<endl;
if (this == &other)
return *this;
num_ = other.num_;
return *this;
}
所以相当于t.operator=(t2)
注:要分清赋值操作与初始化操作
只提供给类的构造函数使用的关键字。
编译器不会把声明为explicit的构造函数用于隐式转换,它只能在程序代码中显示创建对象
/*explicit */Test(int num);
三、构造函数初始化列表、对象成员及其初始化、const成员、引用成员初始化
构造函数的执行分为两个阶段:
1初始化段
2普通计算段(赋值之类的运算)
举个例子:
Clock::Clock(int hour/* =0 */, int minute/* =0 */, int second/* =0 */) : hour_(hour),
minute_(minute), second_(second)//真正的初始化
{
//hour_ = hour;//普通计算段
//minute_ = minute;
//second_ = second;
cout<<"Clock::Clock"<<endl;
}
对象成员及其初始化:
先初始化对象成员,再初始化自身 析构的顺序刚好相反
//给container分配内存时,先给object分配内存
对象成员的构造顺序,只看对象成员的顺序,因为类要实例化,会先实例化内存中的数据成员,再构造成员函数
注:如果对象成员没有默认构造函数,那么在初始化列表中就必须给出
const成员、引用成员的初始化(由一般到特殊)
1、const成员必须在初始化列表进行初始化
2、因为常量分配内存后要马上进行初始化 而如果在函数体内进行初始化时,就是赋值操作,而不是初始化操作!
而引用成员的初始化也只能放在初始化列表之中
3、对象成员(对象所对应的类没有默认构造函数)的初始化,也只能在构造函数的初始化列表中进行
#include<iostream>
using namespace std;
class Object
{
public:
Object(int num = 0):num_(num),kNum_(num),refNum(num)
{
cout<<"Object"<<num<<"..."<<endl;
}
~Object()
{
cout<<"~Object"<<num_<<endl;
}
private:
int num_;
const int kNum_;
int &refNum;
};
int main()
{
Object obj(10);
return 0;
}
如果要每个对象所对应的值都是常量,该如何实现??只能用枚举来进行实现,这样子就不用全局变量来进行控制
注:不想让const仅限于对象内部,想让所有的对象都指向同一个常量
#include <iostream>
using namespace std;
// const成员的初始化只能在构造函数初始化列表中进行
// 引用成员的初始化也只能在构造函数初始化列表中进行
// 对象成员(对象所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行
class Object
{
public:
enum E_TYPE
{
TYPE_A = 100,
TYPE_B = 200
};
public:
Object(int num=0) : num_(num), kNum_(num), refNum_(num_)
{
//kNum_ = 100;
//refNum_ = num_;
cout<<"Object "<<num_<<" ..."<<endl;
}
~Object()
{
cout<<"~Object "<<num_<<" ..."<<endl;
}
void DisplayKNum()
{
cout<<"kNum="<<kNum_<<endl;
}
private:
int num_;
const int kNum_;
int& refNum_;
};
int main(void)
{
Object obj1(10);
Object obj2(20);
obj1.DisplayKNum();
obj2.DisplayKNum();
cout<<obj1.TYPE_A<<endl;
cout<<obj2.TYPE_A<<endl;
cout<<Object::TYPE_A<<endl;
return 0;
}
四、拷贝构造函数
用一个对象来初始化同类的另一个对象 称为拷贝构造函数
Test t(10);
Test t2(t);//如果没有编写 系统为我们提供默认的拷贝构造函数
功能:使用一个已经存在的对象来初始化一个新的同一类型的对象
声明:只有一个参数并且参数为该类对象的引用
如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员
Test(const Test &other);//接收的参数应该是对象的引用
Test::Test(const Test &other):num_(other.num_)
{
}
推荐在初始化列表进行列表 如果在函数体内就是赋值而不是初始化
注:
Test t2 = t
Test t2(t)等价于拷贝构造函数
如果拷贝构造函数中的形参没有用引用的话 会有什么问题呢??
这就涉及到函数初始化时将实参初始化形参的问题
如果不是用引用的话
test other = other,又要调用拷贝构造函数,然后会造成递归,无限循环,而用引用传递的话是不会分配内存的
拷贝构造函数调用的几种情况:
1、当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。
2、当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
针对1:
TestFun2的形参是引用,就不会调用拷贝构造函数:
针对2:
临时对象是马上销毁的,如果是没人接收他的话!!!!所以初始化后直接销毁!
如果是 t = TestFun3(t)的话(是赋值,而不是初始化),赋值成功后,临时对象没有存在的价值,临时对象还是会销毁的
总结:临时对象销毁:1、没人接管 2、赋值成功
如果是初始化操作的话
Test t2 = TestFun3(t);临时对象是不会销毁的(不是赋值,而是初始化)
这个比较特殊,是将临时对象改个名字,
临时对象被引用了,不能销毁它,不然就是一个无效的引用
总结:临时对象不被销毁:1、初始化改名 2、被引用
这时候不会调用拷贝构造函数了 还是原来的t!
但是这样子写会有些问题 必须使用const对象
const Test& TestFun4(const Test& t)
{
//return const_cast<Test&>(t);
return t;
}
Test t2 = TestFun4(t); //这不是无名临时对象变成有名,这是初始化操作,在这一步调用了拷贝构造函数
#include "Test.h"
#include <iostream>
using namespace std;
void TestFun(const Test t)
{
}
void TestFun2(const Test& t)
{
}
Test TestFun3(const Test& t)
{
return t;
}
const Test& TestFun4(const Test& t)
{
//return const_cast<Test&>(t);
return t;
}
int main(void)
{
Test t(10);
//TestFun(t);
//TestFun2(t);
//t = TestFun3(t);
//Test t2 = TestFun3(t);//无名对象改名
//Test& t2 = TestFun3(t);
//Test t2 = TestFun4(t);
const Test& t2 = TestFun4(t);//初始化操作
cout<<"........"<<endl;
return 0;
}
五、深拷贝与浅拷贝、赋值操作、禁止拷贝、空类默认产生的成员
注:strncpy会比strcpy更安全些
String s1("AAA");
s1.Display();
String s2 = s1; // 调用拷贝构造函数
// 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_
此时会出现错误 因为调用了默认的拷贝构造函数 实施的是浅拷贝 等价于s2.str_ = s1.str_;等价于s2与s1指向同一块内存,但是当生存周期结束后,会删除两次内存,所以要提供拷贝构造函数,实现深拷贝,
char* String::AllocAndCpy(char* str)
{
int len = strlen(str) + 1;
char* tmp = new char[len];
memset(tmp, 0, len);
strcpy(tmp, str);
return tmp;
}
String::String(const String& other)
{
str_ = AllocAndCpy(other.str_);
}
如果是要指向同一块内存,要在析构函数做一些处理
深拷贝后还是会有错误!!这就是赋值操作
String s3;
s3.Display();
s3 = s2; // 调用等号运算符
// 系统提供的默认等号运算符实施的是浅拷贝 s3.str_ = s2.str_;
// s3.operator=(s2);
//赋值操作后实施的还是浅拷贝 还是会被删除 所以要提供等号运算符
String& String::operator =(const String &other)
{
if (this == &other)
return *this;
//会产生一个对象 所以要先进行销毁
delete[] str_;
str_ = AllocAndCpy(other.str_);
return *this;
}
禁止拷贝!!! 有些对象是独一无二的 是禁止拷贝给其他对象的,
只需要放在私有,并且不提供它的实现,这样子就无法编译过!!
不要推到运行时刻,能在编译时刻处理就在编译时刻处理
要让对象是独一无二的,我们要禁止拷贝,方法是将拷贝函数与=运算符声明为私有,并且不提供他们的实现
空类默认产生的成员
class Empty {};
Empty(); // 默认构造函数
Empty( const Empty& ); // 默认拷贝构造函数
~Empty(); // 默认析构函数
Empty& operator=( const Empty& ); // 默认赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
在函数后面+const表示:
在这个函数体内不能修改参数的值,只能读取,你若参数赋值,则编译通不过,实际调用时实参的值不变
空类的大小是1个字节,,因为假如没有一个字节的话,如何实例化一个对象呢,
附录代码:
string.h
#ifndef _STRING_H_
#define _STRING_H_
class String
{
public:
String(char* str="");
~String();
String(const String& other);
String& operator=(const String& other);
void Display();
private:
char* AllocAndCpy(char* str);
char* str_;
};
#endif // _STRING_H_
string.cpp
#include "String.h"
//#include <string.h>
#include <cstring>
#include <iostream>
using namespace std;
String::String(char* str/* = */)
{
str_ = AllocAndCpy(str);
}
String::~String()
{
delete[] str_;
}
String::String(const String& other)
{
str_ = AllocAndCpy(other.str_);
}
String& String::operator =(const String &other)
{
if (this == &other)
return *this;
delete[] str_;
str_ = AllocAndCpy(other.str_);
return *this;
}
char* String::AllocAndCpy(char* str)
{
int len = strlen(str) + 1;
char* tmp = new char[len];
memset(tmp, 0, len);
strcpy(tmp, str);
return tmp;
}
void String::Display()
{
cout<<str_<<endl;
}
main.cpp
#include "String.h"
int main(void)
{
String s1("AAA");
s1.Display();
String s2 = s1; // 调用拷贝构造函数
// 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_
String s3;
s3.Display();
s3 = s2; // 调用等号运算符
// 系统提供的默认等号运算符实施的是浅拷贝 s3.str_ = s2.str_;
// s3.operator=(s2);
// 要让对象是独一无二的,我们要禁止拷贝
// 方法是将拷贝构造函数与=运算符声明为私有,并且不提供它们的实现
return 0;
}
empty.cpp
#include <iostream>
using namespace std;
class Empty
{
public:
Empty* operator&()
{
cout<<"AAAA"<<endl;
return this;
}
const Empty* operator&() const
{
cout<<"BBBB"<<endl;
return this;
}
};
int main(void)
{
Empty e;
Empty* p = &e; // 等价于e.operator&();
const Empty e2;
const Empty* p2 = &e2;
cout<<sizeof(Empty)<<endl;
return 0;
}