创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的。
C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。
3.1 构造和析构函数
1. 构造函数和析构函数的概念
有关构造函数
1 构造函数定义及调用
1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
2)构造函数在定义时可以有参数;
3)没有任何返回类型的声明。
2 构造函数的调用
自动调用:一般情况下C++编译器会自动调用构造函数
手动调用:在一些情况下则需要手工调用构造函数
有关析构函数
3 析构函数定义及调用
1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
语法:~ClassName()
2)析构函数没有参数也没有任何返回类型的声明
3)析构函数在对象销毁时自动被调用
4 析构函数调用机制
C++编译器自动调用
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;
class Test
{
public:
Test() //无参数 构造函数
{
a = 10; //作用 完成对属性的初始化构工作
p = (char *)malloc(100);
strcpy(p,"aaaaffff");
cout<<"我是构造函数"<<endl;
}
void print()
{
cout<<p<<endl;
cout<<a<<endl;
}
~Test() //析构函数
{
if (p!=NULL)
{
free(p);
}
cout<<"我是析构函数,被调用了"<<endl;
}
private:
int a;
char *p;
};
//给对象搭建一个舞台,研究对象的行为
void objplay()
{
//构造,先定义的先
//析构 ,先定义的后释放
Test t1;
t1.print();
printf("分隔符\n");
Test t2;
t2.print();
};
void main()
{
//Test t1,t2;
objplay();
cout<<"hello..."<<endl;
system("pause");
return ;
}
2. C++编译器构造析构方案 PK 对象显示初始化方案
设计构造函数和析构函数的原因
面向对象的思想是从生活中来,手机、车出厂时,是一样的。
生活中存在的对象都是被初始化后才上市的;初始状态是对象普遍存在的一个状态的
普通方案:
为每个类都提供一个public的initialize函数;
对象创建后立即调用initialize函数进行初始化。
优缺点分析
1)initialize只是一个普通的函数,必须显示的调用
2)一旦由于失误的原因,对象没有初始化,那么结果将是不确定的
没有初始化的对象,其内部成员变量的值是不定的
3)不能完全解决问题
#include "iostream"
using namespace std;
class Test3
{
public:
void init(int _a,int _b)
{
a=_a;
b=_b;
}
protected:
private:
int a;
int b;
};
void main()
{
//类没有提供 构造函数,C++编译器会自动的提供一个默认的构造函数
//类没有提供构造函数 copy构造函数,C++编译器会自动给 程序员提供一个默认的拷贝构造函数
Test3 t1;
int a=10;
int b=20;
t1.init(a,b);
Test3 tArray[3];
tArray[0].init(1,2);
tArray[1].init(1,2);
tArray[2].init(1,2);
Test3 t21;t21.init(1,2);
Test3 t22;t22.init(1,2);
Test3 t23;t23.init(1,2);
//在这种场景下,显示的初始化方案,显得很蹩脚
Test3 tArray2[3]={t21,t22,t23};
//这种场景下,满足不了编程需求
//Test3 tArray2[1999]={t21,t22,t23};
cout<<"hello..."<<endl;
system("pause");
return ;
}
定义对象数组时,无法显示的初始化
3.2 构造函数的分类以及调用
#include "iostream"
using namespace std;
class Test2
{
public:
Test2() //无参构造函数
{
m_a=0;
m_b=0;
cout<<"无参数构造函数"<<endl;
}
Test2(int a,int b) //有参数构造函数
{
m_a=a;
m_b=b;
cout<<"有参构造函数"<<endl;
}
Test2(int a) //有参数构造函数
{
m_a=a;
m_b=0;
cout<<"有参构造函数"<<endl;
}
//赋值构造函数(copy 构造函数)
Test2(const Test2& obj)
{
cout<<"我也是构造函数"<<endl;
}
public:
void printT()
{
cout<<"普通成员函数"<<endl;
}
protected:
private:
int m_a;
int m_b;
};
void main21()
{
Test2 t1; //调用无参数构造函数
cout<<"hello..."<<endl;
system("pause");
return ;
}
//调用有参构造函数的三种方法
void main()
{
//1 括号法;
Test2 t1(1,2); //调用有参构造函数
t1.printT();
//2 =法
Test2 t2=(3,4,5); //此处C++对等号操作符进行了功能性增强
//C++编译器帮我们自动调用构造函数
//逗号表达式最后一个值为表达式的值
//调用Test2(int a)构造函数
Test2 t3=5;
//3 直接调用构造函数 手动调用构造函数
Test2 t4 = Test2(6,7); //匿名对象 (匿名对象的去和留)
//t4对象的初始化
//直接调用有参构造函数完成t4的初始化
t1=t4; //把t4拷贝给t1 赋值操作
//对象的初始化 和 对象的赋值 是两个不同的概念
cout<<"hello..."<<endl;
system("pause");
return ;
}
1. 无参数构造函数
调用方法: Test t1, t2;
2. 有参构造函数
有参构造函数的三种调用方法:
- c++编译器默认调用有参构造函数 括号法
2)c++编译器默认调用有参构造函数 等号法
3)程序员手工调用构造函数 产生了一个对象 直接调用构造构造函数法
class Test5
{
private:
int a;
public:
//带参数的构造函数
Test5(int a)
{
printf("\na:%d", a);
}
Test5(int a, int b)
{
printf("\na:%d b:%d", a, b);
}
public:
};
int main55()
{
Test5 t1(10); //c++编译器默认调用有参构造函数 括号法
Test5 t2 = (20, 10); //c++编译器默认调用有参构造函数 等号法
Test5 t3 = Test5(30); //程序员手工调用构造函数 产生了一个对象 直接调用构造构造函数法
system("pause");
return 0;
}
3. 拷贝构造函数调用时机
赋值构造函数的四种调用场景(调用时机)
- 第1和第2个调用场景
#include "iostream"
using namespace std;
class Test4
{
public:
Test4() //无参构造函数
{
m_a=0;
m_b=0;
cout<<"无参数构造函数"<<endl;
}
Test4(int a,int b) //有参数构造函数
{
m_a=a;
m_b=b;
cout<<"有参构造函数"<<endl;
}
Test4(int a) //有参数构造函数
{
m_a=a;
m_b=0;
cout<<"有参构造函数"<<endl;
}
//赋值构造函数(copy 构造函数)
Test4(const Test4& obj)
{
cout<<"我也是构造函数"<<endl;
m_b=obj.m_b+100;
m_a=obj.m_a+100;
}
public:
void printT()
{
cout<<"普通成员函数"<<endl;
cout<<"m_a "<<m_a<<" m_a "<<m_b<<endl;
}
protected:
private:
int m_a;
int m_b;
};
//1 赋值构造函数 用1个对象初始化另一个对象
void main()
{
Test4 t1(1,2);
Test4 t0(3,4);
Test4 t2(t1);//2 第二种 t1 初始化t2
t2.printT();
//t0=t1; //用t1给t0赋值 到操作 和 初始化是连个不同的概念
//1 第一种
//Test4 t2=t1; //用t1初始化t2
//t2.printT();
//赋值操作,会不会调用构造函数
//不会调用 operator()
cout<<"hello..."<<endl;
system("pause");
return ;
}
第3个调用场景
#include "iostream"
using namespace std;
class Location
{
public:
Location(int xx=0,int yy=0)
{
X=xx;
Y=yy;
cout<<"Constructoor Object.\n ";
}
//拷贝构造函数,完成对象的初始化
Location(const Location& obj) //拷贝构造函数
{
X=obj.X+1;
Y=obj.Y+1;
}
~Location()
{
cout<<X<<","<<Y<<"Object destoryed."<<endl;
}
int GetX() {return X;}
int GetY() {return Y;}
private:
int X,Y;
};
//业务函数 形参是一个元素
void f(Location p)
{
cout<<p.GetX()<<endl;
}
void playobj()
{
Location a(1,2);//构造
Location b=a;//拷贝
cout<<"b对象已经初始化完毕"<<endl;
f(b);//b实参取初始化形参,会调用copy函数
}
void main()
{
playobj();
cout<<"hello..."<<endl;
system("pause");
return ;
}
第4个调用场景
#include "iostream"
using namespace std;
class Location1
{
public:
Location1(int xx=0,int yy=0)
{
X=xx;Y=yy;
cout<<"Constructoor Object.\n ";
}
//拷贝构造函数,完成对象的初始化
Location1(const Location1& obj) //拷贝构造函数
{
X=obj.X;Y=obj.Y;
}
~Location1()
{
cout<<X<<","<<Y<<"Object destoryed."<<endl;
}
int GetX() {return X;}
int GetY() {return Y;}
private:
int X,Y;
};
//g函数返回一个元素
//1 函数的返回值是一个元素(复杂类型的),返回的是一个新的匿名对象(所以会调用匿名对象类的copy函数)
//2 匿名对象的去和留
//若用匿名对象初始化另外一个同类型的对象,那么匿名对象 转成有名对象
//若用匿名对象 赋值给 另外一个同类型的对象,匿名对象,被析构
//你这么写代码,设计编译器的大牛们:
//我就给你返回一个新对象(没有名字),匿名对象
Location1 g()
{
Location1 A(1,2);
return A;
}
void objplay1()
{
g();
}
void objplay3()
{
//匿名对象初始化m,此时C++编译器 直接把匿名对象转成m,(扶正),从匿名转成有名字m
Location1 m=g();
printf("匿名对象被扶正 ,不会被析构掉 \n");
cout<<m.GetX()<<endl;
}
void objplay4()
{
//用匿名对象 赋值给m2后,从匿名对象被析构
Location1 m2(1,2);
m2=g();
printf("因为匿名对象=给m2, ,匿名对象被析构掉 \n");
cout<<m2.GetX()<<endl;
}
void main()
{
objplay1();
objplay3();
objplay4();
cout<<"hello..."<<endl;
system("pause");
return ;
}
1 函数的返回值是一个元素(复杂类型的),返回的是一个新的匿名对象(所以会调用匿名对象类的copy函数)
2 匿名对象的去和留
若用匿名对象初始化另外一个同类型的对象,那么匿名对象 转成有名对象
若用匿名对象 赋值给 另外一个同类型的对象,匿名对象,被析构
4.默认构造函数
二个特殊的构造函数
1)默认无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
2)默认拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制
3. 构造函数调用规则
1)当类中没有定义任何一个构造函数时,c++编译器会提供默认无参构造函数和默认拷贝构造函数
2)当类中定义了拷贝构造函数时,c++编译器不会提供无参数构造函数
3) 当类中定义了任意的非拷贝构造函数(即:当类中提供了有参构造函数或无参构造函数),c++编译器不会提供默认无参构造函数
4 )默认拷贝构造函数成员变量简单赋值
总结:只要你写了构造函数,那么你必须用。
构造析构阶段性总结:
1)构造函数是C++中用于初始化对象状态的特殊函数
2)构造函数在对象创建时自动被调用
3)构造函数和普通成员函数都遵循重载规则
4)拷贝构造函数是对象正确初始化的重要保证
5)必要的时候,必须手工编写拷贝构造函数
#include "iostream"
using namespace std;
class Text
{
public:
//Text()
//{
// a=0;
// b=0;
// cout<<"无参数构造函数,自动被调用"<<endl;
//}
Text(int _a) //有参构造函数
{
a=_a;
b=0;
}
Text(const Text& obj) //copy构造函数作用:用一个对象初始另一个对象
{
a=obj.a+100;
b=obj.b+100;
}
void printT()
{
cout<<"a: "<<a<<" b: "<<endl;
}
~Text()
{
cout<<"我是析构函数,对象生命周期结束时,会被C++编译器自动调用"<<endl;
}
protected:
private:
int a;
int b;
};
//在定义类时,只要写了构造函数,则必须要用
void main81()
{
//Text t1; //调用无参构造函数
cout<<"hello..."<<endl;
system("pause");
return ;
}
4. 深拷贝和浅拷贝
- 默认复制构造函数可以完成对象的数据成员值简单的复制
- 对象的数据资源是由指针指示的堆时,默认复制构造函数仅作指针值复制
- 显示提供copy构造函数
- 显示操作重载=号操作,不使用编译器提供的浅copy
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;
class Name
{
public:
Name(const char *myp)
{
m_len=strlen(myp);
m_p=(char *)malloc(m_len+1); //字符串后的0
strcpy(m_p,myp);
}
//解决方法,手工的编写拷贝构造函数,使用深拷贝
Name(const Name& obj1)
{
cout<<obj1.m_p<<endl;
m_len = obj1.m_len;
m_p=(char *)malloc(m_len+1);
strcpy(m_p,obj1.m_p);
}
~Name()
{
if (m_p!=NULL)
{
free(m_p);
m_p=NULL;
m_len=0;
}
}
protected:
private:
char *m_p;
int m_len;
};
void objplaymain()
{
Name obj1("abcdefg");
Name obj2=obj1;
Name obj3("obj3");
obj3=obj1; //等号操作
}
void main()
{
objplaymain();
cout<<"hello..."<<endl;
system("pause");
return ;
}
5. 多个对象构造和析构
5.1 对象初始化列表
1)对象初始化列表出现原因
1.必须这样做:
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,
如果没有初始化列表,那么他将无法完成第一步,就会报错。
2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,
因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
2)C++中提供初始化列表对成员变量进行初始化
语法规则
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)
{
// some other assignment operation
}
3)注意概念
初始化:被初始化的对象正在创建
赋值:被赋值的对象已经存在
4)注意:
成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关
初始化列表先于构造函数的函数体执行
#include "iostream"
using namespace std;
class A
{
public:
A(int _a)
{
a=_a;
cout<<"A构造函数"<<"a "<<a<<endl;
}
~A()
{
cout<<"A析构函数"<<endl;
}
protected:
private:
int a;
};
//2 构造函数的初始化列表 解决:在B类中 组合了一个A类对象(A类设计了构造函数)
//根据构造函数的调用规则,设计A的构造函数,必须要用,没有机会初始化A
//新的用法 Constructor::Comstructor():m1(v1),m2(v1,v2),m3(v3)
//3 被组合对象的构造顺序 与定义的顺序有关系,与初始化列表的顺序没有关系
class B
{
public:
B(int _b1,int _b2) : a1(1),a2(2),c(0)
{
}
B(int _b1,int _b2,int m,int n):a1(m),a2(n),c(0)
{
b1=_b1;
b2=_b2;
cout<<"B的构造函数"<<endl;
}
~B()
{
cout<<"B的析构函数"<<endl;
}
protected:
private:
int b1;
int b2;
A a1;
A a2;
const int c;
};
//先执行被组合对象的构造函数
//若组合对象有多个 ,按照定义顺序,而不是按照初始化列表顺序
//析构函数:和构造函数的调用顺序相反
//4 初始化列表 用来给const 属性赋值
void obj10play()
{
A a1(10);
B ojbB(1,2);
//1 参数传递
B ojbB2(1,2,3,4);
//调用顺序
}
void main()
{
obj10play();
cout<<"hello..."<<endl;
system("pause");
return ;
}
1 C++中提供了初始化列表对成员变量进行初始化
2 使用初始化列表出现原因:
-
1).必须这样做:
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,
而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,
如果没有初始化列表,那么他将无法完成第一步,就会报错。 -
2)类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,
因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
6. 构造函数和析构函数的调用顺序研究
构造函数与析构函数的调用顺序
1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数
2)析构函数的调用顺序与对应的构造函数调用顺序相反
7. 类对象的动态建立与释放
7.1 new和delete基本语法
1)在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。
注意: new和delete是运算符,不是函数,因此执行效率高。
2)虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。
new int; //开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针)
new int(100); //开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址
new char[10]; //开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址
new int[5][4]; //开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址
float *p=new float (3.14159);
//开辟一个存放单精度数的空间,并指定该实数的初值为//3.14159,将返回的该空间的地址赋给指针变量p
3)new和delete运算符使用的一般格式为:
用new分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。
4) 应用举例
7.2 类对象的动态建立和释放
使用类名定义的对象都是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。
C++中,可以用new运算符动态建立对象,用delete运算符撤销对象
Box *pt; //定义一个指向Box类对象的指针变量pt
pt=new Box; //在pt中存放了新建对象的起始地址
在程序中就可以通过pt访问这个新建的对象。如
cout<<pt->height; //输出该对象的height成员
cout<<pt->volume( ); //调用该对象的volume函数,计算并输出体积
C++还允许在执行new时,对新建立的对象进行初始化。如
Box *pt=new Box(12,15,18);
这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。
新对象中的height,width和length分别获得初值12,15,18。调用对象既可以通过对象名,也可以通过指针。
在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。只要检测返回值是否为0,就可判断分配内存是否成功。
ANSI C++标准提出,在执行new出现故障时,就“抛出”一个“异常”,用户可根据异常进行有关处理。但C++标准仍然允许在出现new故障时返回0指针值。当前,不同的编译系统对new故障的处理方法是不同的。
在不再需要使用由new建立的对象时,可以用delete运算符予以释放。如
delete pt; //释放pt指向的内存空间
这就撤销了pt指向的对象。此后程序不能再使用该对象。
如果用一个指针变量pt先后指向不同的动态对象,应注意指针变量的当前指向,以免删错了对象。在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。
#include "iostream"
using namespace std;
//1
// malloc free
// new delete 操作符 c++ 语法
//2 new基础类型变量 分配数组变量 分配类对象
//3
void main1301()
{
//
int *p=(int *)malloc(sizeof(int));
*p=10;
free(p);
int *p2=new int; //分配基础类型
*p2=20;
free(p2);
int *p3=new int(30);
printf("*p3:%d \n",*p3);
delete p3;
cout<<"hello..."<<endl;
system("pause");
return ;
}
//分配数组
void main1302()
{
//c语言分配数组
int *p=(int *)malloc(sizeof(int)*10); //int array[10];
p[0]=1;
free(p);
//C++分配数组
int *pArray=new int[10];
pArray[1]=2;
delete [] pArray; //数组不要忘记[]
char *pArray2=new char[25];
delete [] pArray2;
cout<<"hello..."<<endl;
system("pause");
return ;
}
//分配对象
class Test
{
public:
Test(int _a)
{
a=_a;
cout<<"构造函数执行"<<endl;
}
~Test()
{
cout<<"析构函数执行"<<endl;
}
protected:
private:
int a;
};
//分配对象 new delete
//相同 和 不同的地方 new能执行类型构造函数
// delete操作符 能执行类的析构函数
void main()
{
//c语言
Test *pT1=(Test *)malloc((sizeof(Test)));
free(pT1);
//c++
Test *pT2=new Test(10);
delete pT2;
cout<<"hello..."<<endl;
system("pause");
return ;
}
new能执行类型构造函数
delete操作符 能执行类的析构函数