C++ 类和对象、对象的构造和析构、深拷贝和浅拷贝 等问题详解

1前言

C++学习技术路线及目标

研究C++编译器管理类和对象的方法 ===》避免死角

c++编译器对类对象的生命周期管理,对象创建、使用、销毁

c++面向对象模型初探

c++面向对象多态原理探究

操作符重载

C++基础课程学习完毕以后,有没有一个标准,来判断自己有没有入门。

面向抽象类(接口)编程

2类和对象

2.1 基本概念

1)类、对象、成员变量、成员函数

2)面向对象三大概念

封装、继承、多态

3)编程实践

类的定义和对象的定义,对象的使用

求圆形的面积

定义Teacher类,打印Teacher的信息(把类的声明和类的实现分开)

2.2类的封装

1)封装(Encapsulation)

A)封装,是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。

B)封装,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

备注:有2层含义(把属性和方法进行封装 对属性和方法进行访问控制)

C++中类的封装

成员变量,C++中用于表示类属性的变量

成员函数,C++中用于表示类行为的函数

2)类成员的访问控制

在C++中可以给成员变量和成员函数定义访问级别

Public修饰成员变量和成员函数可以在类的内部和类的外部被访问

Private修饰成员变量和成员函数只能在类的内部被访问

//类是把属性和方法封装 同时对信息进行访问控制

//类的内部,类的外部

//我们抽象了一个类,用类去定义对象

//类是一个数据类型,类是抽象的

//对象是一个具体的变量。。占用内存空间。

class Circle

{

public:

double r;

double s;

public:

double getR()  

{

a++;

return r;

}

void setR(double val)

{

r = val;

}

public:

double getS() //增加功能时,是在修改类, 修改类中的属性或者是方法

{

 s = 3.14f*r*r;

 return s;

}

//private:

int a;

};

3)struct和class关键字区别

在用struct定义类时,所有成员的默认属性为public

在用class定义类时,所有成员的默认属性为private

对象的构造和析构

创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的。

为了解决这个问题,C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。

3.1构造和析构函数

1构造函数和析构函数的概念

有关构造函数

1构造函数定义及调用

1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;

2)构造函数在定义时可以有参数;

3)没有任何返回类型的声明。

2构造函数的调用

自动调用:一般情况下C++编译器会自动调用构造函数

手动调用:在一些情况下则需要手工调用构造函数

有关析构函数

3)析构函数定义及调用

1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数

语法:~ClassName()

2)析构函数没有参数也没有任何返回类型的声明

3)析构函数在对象销毁时自动被调用

4)析构函数调用机制

C++编译器自动调用

代码演示:dm01_构造函数的基础.cpp

2 C++编译器构造析构方案 PK 对象显示初始化方案

设计构造函数和析构函数的原因 

面向对象的思想是从生活中来,手机、车出厂时,是一样的。

生活中存在的对象都是被初始化后才上市的;初始状态是对象普遍存在的一个状态的

普通方案:

为每个类都提供一个public的initialize函数;

对象创建后立即调用initialize函数进行初始化。

优缺点分析

1)initialize只是一个普通的函数,必须显示的调用

2)一旦由于失误的原因,对象没有初始化,那么结果将是不确定的

没有初始化的对象,其内部成员变量的值是不定的

3)不能完全解决问题

//为什么对象需要初始化 有什么样的初始化方案

#include "iostream"

using namespace std;

/*

思考为什么需要初始化

面向对象思想来自生活,手机、车、电子产品,出厂时有初始化

怎么样进行初始化?

方案1:显示调用方法

缺点:易忘、麻烦;显示调用init,不能完全解决问题

*/

class Test21

{

public:

int m;

int getM() const { return m; }

void setM(int val) { m = val; }

int n;

int getN() const { return n; }

void setN(int val) { n = val; }

public:

int init(int m,int n)

{

this->m = m;

this->n = n;

return 0;

}

protected:

private:

};

int main()

{

int rv =0;

Test21 t1;  //无参构造函数的调用方法

Test21 t2;

//t1.init(100, 200);

//t2.init(300, 400);

cout<<t1.getM()<<" "<<t1.getN()<<endl;

cout<<t2.getM()<<" "<<t2.getN()<<endl;

//定义对象数组时,没有机会进行显示初始化

Test21 arr[3];

//Test arr_2[3] = {Test(1,3), Test(), Test()};

system("pause");

return rv;

}

3.2构造函数的分类及调用

C++编译器给程序员提供的对象初始化方案,高端大气上档次。

//有参数构造函数的三种调用方法

class Test

{

private:

int a;

int b;

public:

//无参数构造函数

Test()

{

;

}

//带参数的构造函数

Test(int a, int b)

{

;

}

//赋值构造函数

Test(const Test &obj)

{

;

}

public:

void init(int _a, int _b)

{

a = _a;

b = _b;

}

};

1无参数构造函数

调用方法: Test t1, t2;

2有参构造函数

有参构造函数的三种调用方法 

//有参数构造函数的三种调用方法

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 AA

{

public:

AA() //无参构造函数 默认构造函数

{

cout<<"我是构造函数,自动被调用了"<<endl;

}

AA(int _a) //无参构造函数 默认构造函数

{

a = _a;

}

  AA(const AA &obj2)

  {

  cout<<"我也是构造函数,我是通过另外一个对象obj2,来初始化我自己"<<endl;

  a = obj2.a + 10;

  }

~AA()

{

cout<<"我是析构函数,自动被调用了"<<endl;

}

void getA()

{

printf("a:%d \n", a);

}

protected:

private:

int a;

};

//单独搭建一个舞台

void ObjPlay01()

{

AA a1; //变量定义

//赋值构造函数的第一个应用场景

//用对象1 初始化 对象2

AA a2 = a1; //定义变量并初始化 //初始化法

a2 = a1; //用a1来=号给a2 编译器给我们提供的浅copy

}

第二个应用场景

//单独搭建一个舞台

void ObjPlay02()

{

AA a1(10); //变量定义

//赋值构造函数的第一个应用场景

//用对象1 初始化 对象2

AA a2(a1); //定义变量并初始化 //括号法

//a2 = a1; //用a1来=号给a2 编译器给我们提供的浅copy

a2.getA();

//注意:初始化操作 和 等号操作 是两个不同的概念

第3个调用场景

#include "iostream"

using namespace std;

class Location

{

public:

Location( int xx = 0 , int yy = 0 )

{

X = xx ;  Y = yy ;  cout << "Constructor Object.\n" ;

}

Location( const Location & p )     //复制构造函数

{

X = p.X ;  Y = p.Y ;   cout << "Copy_constructor called." << endl ;  

}

~Location()

{

cout << X << "," << Y << " Object destroyed." << endl ;

}

int  GetX () { return X ; } int GetY () { return Y ; }

private :   int  X , Y ;

} ;

//alt + f8 排版

void f ( Location  p )   

{

cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;

}

void mainobjplay()

{  

Location A ( 1, 2 ) ;  //形参是一个元素,函数调用,会执行实参变量初始化形参变量

f ( A ) ;

}

void main()

{  

mainobjplay();

system("pause");

}

第4个调用场景 

第四个应用场景

#include "iostream"

using namespace std;

class Location

{

public:

Location( int xx = 0 , int yy = 0 )

{

X = xx ;  Y = yy ;  cout << "Constructor Object.\n" ;

}

Location( const Location & p )     //复制构造函数

{

X = p.X ;  Y = p.Y ;   cout << "Copy_constructor called." << endl ;  

}

~Location()

{

cout << X << "," << Y << " Object destroyed." << endl ;

}

int  GetX () { return X ; } int GetY () { return Y ; }

private :   int  X , Y ;

} ;

//alt + f8 排版

void f ( Location  p )   

{

cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;

}

Location g()

{

Location A(1, 2);

return A;

}

//对象初始化操作 和 =等号操作 是两个不同的概念

//匿名对象的去和留,关键看,返回时如何接

void mainobjplay()

{  

//若返回的匿名对象,赋值给另外一个同类型的对象,那么匿名对象会被析构

//Location B;

//B = g();  //用匿名对象 赋值 给B对象,然后匿名对象析构

//若返回的匿名对象,来初始化另外一个同类型的对象,那么匿名对象会直接转成新的对象

Location B = g();

cout<<"传智扫地僧测试"<<endl;

}

void main()

{  

mainobjplay();

system("pause");

}

4默认构造函数

二个特殊的构造函数

1)默认无参构造函数

当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空

2)默认拷贝构造函数

当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制

3.3构造函数调用规则研究

1)当类中没有定义任何一个构造函数时,c++编译器会提供默认无参构造函数和默认拷贝构造函数

2)当类中定义了拷贝构造函数时,c++编译器不会提供无参数构造函数

3) 当类中定义了任意的非拷贝构造函数(即:当类中提供了有参构造函数或无参构造函数),c++编译器不会提供默认无参构造函数

4 )默认拷贝构造函数成员变量简单赋值

总结:只要你写了构造函数,那么你必须用。

构造析构阶段性总结

1)构造函数是C++中用于初始化对象状态的特殊函数

2)构造函数在对象创建时自动被调用

3)构造函数和普通成员函数都遵循重载规则

4)拷贝构造函数是对象正确初始化的重要保证

5)必要的时候,必须手工编写拷贝构造函数

========》1个对象的初始化讲完了,增加一个案例。

3.4深拷贝和浅拷贝

  • 默认复制构造函数可以完成对象的数据成员值简单的复制
  •  对象的数据资源是由指针指示的堆时,默认复制构造函数仅作指针值复制
1浅拷贝问题抛出和分析

深拷贝浅拷贝现象出现的原因

2浅拷贝程序C++提供的解决方法

显示提供copy构造函数

显示操作重载=号操作,不使用编译器提供的浅copy

class Name

{

public:

Name(const char *pname)

{

size = strlen(pname);

pName = (char *)malloc(size + 1);

strcpy(pName, pname);

}

Name(Name &obj)

{

//用obj来初始化自己

pName = (char *)malloc(obj.size + 1);

strcpy(pName, obj.pName);

size = obj.size;

}

~Name()

{

cout<<"开始析构"<<endl;

if (pName!=NULL)

{

free(pName);

pName = NULL;

size = 0;

}

}

void operator=(Name &obj3)

{

if (pName != NULL)

{

free(pName);

pName = NULL;

size = 0;

}

cout<<"测试有没有调用我。。。。"<<endl;

//用obj3来=自己

pName = (char *)malloc(obj3.size + 1);

strcpy(pName, obj3.pName);

size = obj3.size;

}  

protected:

private:

char *pName;

int size;

};

//对象的初始化 和 对象之间=号操作是两个不同的概念

void playObj()

{

Name obj1("obj1.....");

Name obj2 = obj1; //obj2创建并初始化

Name obj3("obj3...");

//重载=号操作符

obj2 = obj3; //=号操作

cout<<"业务操作。。。5000"<<endl;

}

void main61()

{

playObj();

system("pause");

}

3.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)注意:

成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关

初始化列表先于构造函数的函数体执行

/*

1 C++中提供了初始化列表对成员变量进行初始化

2 使用初始化列表出现原因:

1.必须这样做:

如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,

而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,

如果没有初始化列表,那么他将无法完成第一步,就会报错。

2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值

当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,

因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

*/

//总结 构造和析构的调用顺序

#include "iostream"

using namespace std;

class ABC

{

public:

ABC(int a, int b, int c)

{

this->a = a;

this->b = b;

this->c = c;

printf("a:%d,b:%d,c:%d \n", a, b, c);

printf("ABC construct ..\n");

}

~ABC()

{

printf("a:%d,b:%d,c:%d \n", a, b, c);

printf("~ABC() ..\n");

}

protected:

private:

int a;

int b;

int c;

};

class MyD

{

public:

MyD():abc1(1,2,3),abc2(4,5,6),m(100)

//MyD()

{

cout<<"MyD()"<<endl;

}

~MyD()

{

cout<<"~MyD()"<<endl;

}

protected:

private:

ABC abc1; //c++编译器不知道如何构造abc1

ABC abc2;

const int m;

};

int run()

{

MyD myD;

return 0;

}

int main_dem03()

{

run();

system("pause");

return 0;

}

3.6构造函数和析构函数的调用顺序研究

构造函数与析构函数的调用顺序

1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数

2)析构函数的调用顺序与对应的构造函数调用顺序相反

3.7构造函数和析构函数综合练习

通过训练,把所学知识点都穿起来

1构造析综合训练

demo10_构造析构练习强化.cpp (讲解)

展示分析过程,注意赋值构函数的调用

2匿名对象强化训练

demo10_构造析构练习强化.cpp

  1. 匿名对象生命周期
  2. 匿名对象的去和留
3匿名对象强化训练
  1. 构造中调用构造

demo11_匿名对象练习强化.cpp

构造函数中调用构造函数,是一个蹩脚的行为。

3.8 对象的动态建立和释放

1 new和delete基本语法

1)在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。

注意: new和delete是运算符,不是函数,因此执行效率高。

2)虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。new运算符的例子:


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,用户可以根据该指针的值判断分配空间是否成功。

  1. 应用举例
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运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。

3 编程实践

//1  malloc free函数 c关键字

// new delete 操作符号 c++的关键字

//2 new 在堆上分配内存  delete

//分配基础类型 、分配数组类型、分配对象

//3 new和malloc 深入分析

混用测试、异同比较

结论: malloc不会调用类的构造函数

Free不会调用类的析构函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高亚奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值