C++郑莉

本文介绍了C++中的基本数据类型、变量、常量和运算符,详细讲解了函数的声明、定义、调用、参数传递以及内联函数的概念。此外,还涵盖了面向对象的特性,如抽象、封装、继承和多态,以及类、对象、构造函数和析构函数的使用。文章强调了在编程中函数调用栈的工作原理和类型安全的重要性。
摘要由CSDN通过智能技术生成

2.2

2.2.1基本数据类型

类型名长度取值范围
char1-128~
bool1false, true
unsigned char10~255
short2-32768~32767
int4
long8

2.2.2 常量

1.整型常量

10进制正常
8进制数字0开头
16进制数字0x开头

2.实型常量

分为一般形式和指数形式

一般形式: 12.5

指数形式:0.345E+2

3.字符常量

单引号括起来的一个字符

’a‘, 'd’等

4.字符串常量

双引号括起来

“dajkljwdkl”

2.2.3 变量

1.变量的声明和定义

1.变量在使用之前首先要声明其类型和名称

​ 声明一个变量只是将变量名标识符的有关信息告诉编译器,使得编译器认识该标识符但是不涉及内存的分配。而定义一个变量才是给变量分配内存空间

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,通过extern关键字声明变量而不是定义,即不分配存储空间。

在定义一个变量的同时也可以同时赋予初值,实质上就是给对应的内存空间赋予初值

例如:

int a = 3;
double f = 3.56;
char c = 'a';

2.变量的存储类型

变量除了具有数据类型还有存储类型

存储类型存放位置
auto堆栈方式分配,暂时性存储,存储空间可以 多次覆盖使用
register存放在通用寄存器
extern所有函数和程序段都能引用
static内存中以固定地址存放,整个程序运行过程都有效

2.2.4符号常量

符号常量使用之前一定要首先声明

	const 数据类型说明符 常量名=常量值
或
	数据类型说明符 const 常量名=常量值
	
	例如
	const float PI=3.14

使用符号常量有利于程序的可读性,且避免因修改常量值带来的不一致性

2.2.5运算符与表达式

1.隐式转换

char->short->int ->unsigned->long->unsigned long ->float->double

(书本35页)

2.显式转换

类型说明符 (表达式)
	float z = 7.56;
	int d;
	d = int(z);

标准C++定义了4种类型转换操作符

static_cast<类型说明符>(表达式)

C++中的static_cast执行非多态的转换,用于代替C中通常的转换操作。因此,被做为显式类型转换使用。

int i;
float f = 166.71;
i = static_cast<int>(f);
等同于 i = int(f);

dynamic_cast.<类型说明符>(表达式)

const_cast<类型说明符>(表达式)

reinterpret_cast<类型说明符>(表达式)

(后续会介绍)

显式转换是不安全的,可能精度会损失,且是暂时的一次性的

2.3 数据的输入和输出

2.3.1 I/O流

C++中将数据从一个对象到另外一个对象的流动抽象为流,流在使用前被创立,使用后要被删除。从流中获取数据的操作称为提取,向流中插入添加数据称为插入操作

输入输出通过I/O流实现,cin和cout是预定义的流类对象

cin用来处理标准输入,即键盘输入

cout即屏幕输出

2.3.2预定义的插入符和提取符

“<<"是预定义的插入符,作用在流类对象cout上即可以实现屏幕输出

”>>"最一般的数据提取符作用在流类对象cin实现键盘输入

int a,b;
cin>>a>>b;

2.5 自定义数据类型

C++允许用户自定义数据类型

2.5.1 typedef 声明

给已有的数据类型起一些有具体意义的别名有利于程序的可读性。

给较长的类型名起短的别名有利于程序的简洁

typedef 已有类型名 新类型名表;
其中新类型名表可以有多个标识符用逗号分隔
typedef double Area,Volume

2.5.2枚举类型enum

enum 枚举类型名 {变量值列表};
例如 
	enum Weekday {SUN, MON, TUE, WED, THU, FRI, SAT};
	

枚举类型应用说明:

  • 对枚举元素按常量处理,不能对它们赋值。例如

    SUN=0;//SUN是枚举元素,此语句非法

  • 枚举元素具有默认值分别是0,1,2,3,4…例如SAT为6

  • 整型到枚举数据的转换需要采用显式转换

  • 枚举数据到整型可以用隐式转换

    • enum DAY
      {
            MON=1, TUE, WED, THU, FRI, SAT, SUN
      };
       
      int main()
      {
          enum DAY day;
          day = WED;
          printf("%d",day);//3
      }
      
      

2.6 深度探索

2.6.1 变量实现机制

C++源程序中,之所以要使用变量名,是为了把不同的变量区别开。在运行程序时,C++变量的值都存储在内存中,内存的每一个单元都有一个唯一的编号,就是它的地址。不同内存单元的地址互不相同

声明一个变量,一方面告诉编译器这个名字表示一个变量,另一方面也指出它的类型,一个变量只有声明后才能使用。

如果一个变量声明的同时又是一个变量的定义,意味着它指明变量类型的同时还确立了变量地址的分配位置。在不同位置定义变量意味着为变量分配不同的地址。

3.1函数的定义和使用

3.1.1函数的定义

函数定义的语法形式,形参和返回值

3.1.2函数的调用

1.函数的调用形式

变量在使用之前都要声明,函数在调用之前也要声明。

函数的定义就属于函数的声明,因此在定义一个函数以后就可以直接调用这个函数,但如果希望在定义一个函数前调用它,需要在调用函数之前添加该函数的函数原型声明。

类型说明符 函数名 函数名(含类型说明的形参表);
函数声明时形参的名可以省略,只用写形参类型

与变量的声明和定义类似,声明一个函数只是把函数有关信息(函数名,形参表,返回值)告诉编译器,不产生任何代码,定义一个函数时除了同样要给出函数的有关信息外,还要写出函数的代码。

假如函数定义在函数调用之前,可以不用函数声明

否则需要在调用之前添加函数声明。

2.嵌套调用

函数允许嵌套调用。

如果函数1调用了函数2,函数2再调用函数3,就形成了函数的嵌套调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MN7Rmnf2-1673630374437)(.\QQ图片20230112182427.jpg)]

3.递归调用

递归算法的实质是将原有问题分解为新的 问题,而解决新问题时又用到原有问题的解法。

第一阶段:递推。将原问题不断分解为新的子问题,从未知推到已知的条件即为递归结束的条件

第二阶段: 回归。从已知条件出发,按照递推的逆过程,逐一求值回归,最后达到递推的开始处结束递推

#include<iostream>
using namespace std;
unsigned fac(unsigned n) {
	unsigned f;
	if (n == 0)//递推到已知时,n=0,0的阶乘为1
		return 1;
	else f = fac(n - 1) * n;
	return f;
}
int main() {
	unsigned n;
	cin >> n;
	unsigned y = fac(n);
	cout << y << endl;
	return 0;
	
}
int getKfromN(int n ,int k){
    if(n<k){ 
        return 0;
    }else if(n==k || k==0){
        return 1;
    }else{
        return getKfromN(n-1,k-1)+getKfromN(n-1,k);
    }
}

3.1.3函数的参数传递

在函数未被调用的时候,形参并不占用实际的内存空间,也没有实际的值,只有当函数被调用时才为形参分配存储单元,并将实参和形参结合。每个实参都是一个表达式,其类型必须与形参符合。

函数的参数传递指的是形参和实参结合的过程,有值传递和引用传递

1.值传递

值传递是指发生函数调用时,给形参分配内存空间,并用实参为形参初始化。这一过程是参数值的单向传递过程,当形参获得值便与实参脱离关系,无论形参怎么发生变化都不会影响到实参。

2.引用传递

如何使在子函数中对形参做的更改能对主函数的实参有效呢,这需要引用传递。

引用是一种特殊类型的变量,可以被认为是令一变量的别名,通过对引用的操作,和对实参的操作效果一样。

int i,j;
int &ri = i;//建立一个int型的引用ri,将其初始化为变量i的一个别名。
j=10;
ri=j;//相当于i=j;

使用声明时要注意以下问题

  • 声明一个引用时,必须同时对它初始化,使它指向一个已经存在的对象。
  • 一旦一个引用初始化后,就不能改为指向其他对象。
  • 也就是说一个引用自诞生起就必须确定是哪个变量的别名。而且始终是这一个变量的别名。

引用也可以作为形参,如果将引用作为形参,则情况稍有不同。

形参的初始化不在类型说明中进行,而是在执行主调函数中的调用表达式时,才为形参分配内存空间,同时用实参来初始化形参。

这样引用类型的形参就可以通过形参实参结合,成为了实参的一个别名,对形参的任何操作都可以直接作用于实参。

3.2内联函数

使用函数有利于代码的重用,可以提高开发效率,增强程序的可靠性也便于分工合作,修改维护。但是函数调用也会降低程序的执行效率增加实际和空间方面的开销。因此对于一些功能简单规模小但又频繁使用的函数,可以设计为内联函数。

内联函数:在编译时将函数体嵌入在每一个调用处。

而不是在调用时发生控制转移。

这样就节省了参数传递,控制转移等开销。

内联函数的定义和普通函数的定义方式几乎一样,只是需要使用关键字inline,其语法形式如下

inline 类型说明符 函数名(含类型说明的形参表)
{
语句序列
}

inline关键字只是表示一个要求,编译器并不承诺inline修饰的函数一定可以作为内联函数。

而在现代编译器中,没有用inline修饰的函数也可能被编译为内联函数,

通常内联函数都是比较简单的函数

如果将比较复杂的函数定义为内联函数反而会造成代码膨胀增大开销。

有些函数是肯定不能作为内联函数处理的,比如存在自身对自身的递归调用的函数。

inline double calArea(double radius){
	return PI*radius*radius;
}
int main(){
	double r = 3.0;
	//调用内联函数求圆的面积,编译时将此处替换成calArea函数体语句
	double area = calArea(r);
	cout<<area<<endl;
	return 0 ;
		
}

3.3带默认形参值的函数

函数在定义时可以预先声明默认的形参值。

调用时如果给出形参,则用实参初始化形参,如果没有给出实参,则采用预先声明的默认形参值。

例如:

int add(int x = 5, int y=6){
	return x+y;
}
int main(){
	add(10,20);//10+20
	add(10);//10+6
	add();//5+6
}

有默认值地形参必须放在形参列表的最后,也就是说,在有默认值的形参右边不能出现无默认值的形参。因为在函数调用中,实参和形参是按左往右的顺序建立对应关系的。

在相同的作用域内,不允许在同一函数的多个声明中对同一个参数的默认值重复定义,即使前后定义的值相同也不行。

注意,函数的定义也属于函数声明。故,在一个函数定义之前有函数声明,默认形参值需要在原型声明中给出,在函数定义时不能再出现默认形参值。

int add(int x =5,int y = 6);//默认形参值在函数声明中给出
int main(){
	add();
	return 0;
}
int add(int x/*=5*/, int y/*=6*/){//函数定义,在这里不能出现默认形参值
	return x + y;
}
像在函数定义处在形参表中以注释来说明参数的默认值是一种好习惯

3.4函数重载

一个函数就是一个操作的名字,C++对于相同的函数名提供了函数重载,

两个以上的函数具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的个数的最佳匹配自动确定调用哪一个函数,这就是函数的重载。

如果没有重载机制,那么对不同类型的数据进行相同的操作也需要定义名称完全不同的函数。

当使用具有默认形参值的函数重载形式时,需要注意避免二义性。

void fun(int length, int width=2, int height = 3);
void fun(int length);
当调用fun(1);时编译器就无法确定应该执行哪个重载函数。

C++是怎样来支持函数重载呢。而C语言不支持,原因在于C和c++,他们之间对源程序编译技术不一样,C++编译器编译源文件时通过底层倾轧(name mangling)技术

底层倾轧(name mangling) ---- 将原有函数名 + 参数类型 ----> 在底层时,形成一个新的函数名,从底层,各个函数名还是不一样的。

比如:

void print(int data) —> void print_i(int data)

void print(int data,int data2) —> void print_ii(int data,int data2)

void print(char data) ---- void print_c(char data)

以上三个函数通过函数重载,实现了静态联编,从而让函数体现出了多种形态—也被称之为静态多态

3.5C++系统函数

调用函数之前必须先加以声明。

系统函数的原型声明已经全部由系统提供。

用include指令嵌入相应的头文件,然后就可以使用系统函数。

3.6深度探索

3.6.1运行栈和函数调用的执行

1.运行栈的工作原理

全局变量,用一个唯一的确定的地址定位

局部变量,只在调用它所在的函数时才会生效,一旦函数返回后就会失效。

1.很多局部变量的生存周期小于整个程序的生存周期,如果为每个局部变量分配不同的空间,则空间的利用率降低。

2.函数递归调用时,存在一个函数返回但是另外一个函数调用又发生的情况,对于多次调用,相同名称的局部变量的值不同,所以必然再不同的地址。

函数形参的情形与局部变量非常相似,它们不能像全局变量那样采用固定地址加以定位,需要存储在一种特殊的结构中。就是栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Co2gCUy3-1673630374438)(C:\Users\Administrator\Desktop\QQ图片20230112182427.jpg)]

如图所示,越早开始的调用返回的越晚。

函数调用中的形参和局部变量在调用开始时生效,函数返回后失效。它们有效时长和函数调用期间是重合的。符合栈的特性,这样很自然的函数的形参和实参可以用栈来存储,这种栈叫做运行栈。

这种栈实际上是一段区域的内存空间,,与存储全局变量的空间没有什么不同,只是寻址方式不同。

运行栈中的数据分为一个个栈帧,每一个栈帧对应一次函数调用。

函数的每次调用,都有它自己独立的栈帧。栈帧中维持着函数调用所需要的各种信息,包括函数的入参、函数的局部变量、函数执行完成后下一步要执行的指令地址、寄存器信息等。

每次函数调用都有一个栈帧被压入运行栈中,而调用返回后相应的栈帧要被弹出。

一个函数在执行过程中能够直接随机访问它所对应的栈帧中的数据,即处在栈最顶端的栈帧的数据。

一个函数调用其他函数时,要为它所调用的函数设置实参,具体方式是在调用前将实参值压入栈中,运行栈的这一部分空间是主调函数和被调函数都可以直接访问的,参数的形实结合就是通过访问这一公共空间完成的。虽然这一个函数在调用时的形参和局部变量地址是不确定的,但它们的地址相对于栈顶地址却是确定的。这样就可以通过栈顶的地址定位形参和局部变量,即栈顶指针。

函数的返回地址和参数

临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量

每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每个独立的栈帧一般包括:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 函数调用的上下文
    栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部;
    ebp 寄存器又被称为帧指针(Frame Pointer);
    esp 寄存器又被称为栈指针(Stack Pointer);
  • 在将数据压入和弹出运行栈、确定要访问的形参和局部变量的地址时,都需要获得栈顶的地址,esp寄存器就是用来寄存栈顶的地址
  • 有些函数的栈帧大小是不确定的,这就会在函数返回前恢复栈指针遇到麻烦,所以还需要另外一个寄存器保存函数刚被调用时,栈指针的位置,用ebp完成。帧指针。
  • 由于形参和局部变量相对于帧指针的位置是确定的,所以函数的形参和局部变量的地址常常通过帧指针来计算。而非栈地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2E7voQZk-1673630374439)(./QQ图片20230112182427.jpg)]

在函数调用的过程中,有函数的调用者(caller)和被调用的函数(callee).
调用者需要知道被调用者函数返回值;
被调用者需要知道传入的参数和返回的地址;

3.6.2函数声明和类型安全

不同类型的数据在内存中都以二进制序列表示,在运行时并没有保存它的类型信息,有关类型的特性都蕴含在数据所执行的操作之中,故在使用变量之前要必须声明,这样才可以为该变量所参与的每一个操作赋予完整的意义。

如在调用一个函数前必须声明函数原型,则能够避免向一个函数传递数量不正确或类型不正确的参数。因为在事先声明函数原型的情况下,在执行函数调用的时候若传递的参数不正确编译器很容易就检测出来。

第四章 类与对象

4.1面向对象程序设计的基本特点

4 .1.1抽象

抽象是指对具体问题进行概括,抽出这一类对象的公共特性并加以描述。

两个方面:数据抽象和行为抽象

对于时钟程序
数据抽象:
int hour, int minute, int second
功能抽象:
showtime(), settime();

4.1.2封装

将抽象得到的数据和行为相结合,形成类,其中的数据和函数就是类的成员

按C++语法

class Clock//class关键字,类名
{
public:		//外部接口
	void setTime(int newH,int newM, int newS );//行为,代码成员
	void showTime();
private:	//特定的访问权限
	int hour, minute, second;//属性,数据成员
};//加分号。 边界

通过封装将一部分成员充当类和外部的接口,将其他成员隐蔽起来,这样就达到了对成员访问权限的合理控制,使得不同类之间的相互影响降低到最低程度。

将代码封装为一个可重用的程序模块,在编写程序时就可以利用已有的成果。

由于有外部接口,依据特定的访问规则就可以使用封装好的模块,不必了解类的实现细节。

4.1.3继承

C++语言中提供了类的继承机制,允许程序员在保持原有类的特性基础上进行更具体的说明

4.1.4多态

多态性在C++中可以通过强制多态,重载多态,类型参数化多态,包含多态四种。

强制多态:通过将一种类型的数据转换成另一种类型的数据

包含型多态和类形参数化多态属于一般多态性,是真正的多态型。

C++中采用虚函数实现包含多态,虚函数是多态性的精华。

模板是C++中实现参数化多态的工具,分为函数模板和类模板

4.2 类和对象

4.2.2类成员的访问控制

类的成员包括数据成员和函数成员。

对类成员的访问控制分为 public private protected

公有类成员定义了类的外部接口,类外只能访问类的公有成员。

private后面声明的就是类的私有成员。私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的

保护类型的成员性质和私有成员的性质相似,差别在于继承过程中产生的影响不同。

4.2.3对象

clock myclock;//声明了一个时钟类型的对象

对象所占据的内存空间只有数据成员,函数成员不在每一个对象中存储副本,每个函数的代码在内存中只占据一份空间。

定义类和对象,可以访问对象的成员,比如设置和显示对象myClock的时间,采用“.”操作符

类的外部只能访问类的公有成员,类的成员函数中可以访问类的全部成员。

4.2.4类的成员函数

1.成员函数的实现。

函数原型的声明写在类体中,而函数的具体实现写在类的定义之外。

实现成员函数时,要指明类的名称

返回值类型  类名::函数成员名(参数表){
	函数体
}


例如
void Clock::setTime(int newH,int newM,int newS){
	hour = newH;
	minute = newM;
	second = newS;
}
void Clock::showTime(){
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}

2.成员函数调用中的目的对象

调用一个成员函数"."指出调用所针对的对象,这以对象在本次调用中称为目的对象。

myClock.showTime();//myClock就是目的对象

4.内联成员函数

内联函数的声明有两种方式:隐式声明和显示声明

将函数体直接放在类中,称为隐式声明比如将showTime()声明为内联函数可以写作

class Clock
{
public:
	void showTime(){
		cout<<hour<<":"<<minute<<":"<<second<<endl;
	}
private:
    int hour, minute, second;
}
inline void Clock::showTime()
{
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}
效果相同

4.3 构造函数和析构函数

类和对象的关系相当于基本数据类型和变量的关系。

在定义对象的同时可以对数据成员赋初值。

在定义对象的时候对数据成员进行设置称为对象的初始化

在特定对象用完以后,还需要进行一些清理工作。

C++程序的初始化和清理工作分别由两个特殊的成员函数完成,即构造函数和析构函数。

4.3.1构造函数

要理解构造函数,先理解对象的建立过程

每一个变量在程序运行时都要占据一定内存空间,在声明一个变量时对变量进行初始化,就意味着为变量分配内存单元的同时在其中写入了变量的初始值。这样的初始化在C++源程序看似简单,实际上编译器却需要根据变量的类型自动产生一些代码完成初始化操作

对象的建立过程也是类似。在程序执行过程中,当遇到对象声明语句时,程序向操作系统申请一定内存空间存放新建的对象。

类的对象太复杂没编译器不知道如何产生代码来完成初始化

因此,如果需要进行对象初始化,需要程序员自己编写初始化程序,如果程序员没有自己编写初始化程序,却在声明对象时贸然指定对象初始值,不仅不能完成初始化还会引起编译的错误。

C++严格规定了初始化程序的接口形式,并有一套自动的调用机制。

这里的初始化程序就是构造函数。

构造函数的作用是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。

构造函数也是类的一个成员函数,除了具有一般成员函数的特征外还有一些特殊的性质:

  • 构造函数的函数名和类名相同,且没有返回值

  • 构造函数通常为公有函数

  • 只要类中有构造函数,编译器就会在创建新对象的地方自动插入对构造函数调用的代码,因此,通常说构造函数在对象被创建的时候将自动被调用。

    调用时无须提供参数的构造函数称为默认构造函数

    如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数,该函数的参数列表和函数体都为空,如果类中声明了构造函数,编译器便不会再为之生成隐含的构造函数。

class Clock{
public:
	Clock(){
	
	}/*编译系统生成的隐含的默认构造函数*/
};

这个构造函数不做任何事,但再建立对象时 自动调用构造函数是C++程序例行公事的必然行为。

有时函数体为空的构造函数并非不做任何事,因为它还要负责基类的构造和成员对象的构造。

如果程序员定义恰当的构造函数,Clock类对象在建立的时候就能获得一个初始的时间值。

class Clock{
public:
	Clock(int newH, int newM, int newS);//构造函数
private:
	int hour,minute,second;
}
Clock::Clock(int newH, int newM, int newS){
	hour=newH;
	minute = newM;
	second = newS;
}

假如定义了构造函数,那么编译系统就不会生成隐含的默认构造函数。

自定义的构造函数带有形参,则

Clock c2;//出错,因为没有给出必要的实参

同样的构造函数也可以重载。

4.3.2复制构造函数

面向对象的程序设计,对象的复制是C++必不可少的功能。

生成对象的副本,就是复制构造函数的功能。

复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形态是本类的对象的引用其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化一个同类的对象

程序员可以根据实际问题的需要定义特定的复制构造函数,以实现同类数据对象之间的数据成员的传递。如果程序员没有定义类的复制构造函数,系统就会在必要的时候自动生成隐含的复制构造函数。

这个隐含的复制构造函数功能是把初始值对象每个数据成员的值都复制到新建立的对象中。

class 类名{
public:
	类名(形参表);//构造函数
	类名(类名 &对象名);//复制构造函数
}
类名::类名(类名&对象名){//复制构造函数的实现
    函数体
}

下面是一个例子

class Point{
public:
	Point(int xx=0; int yy=0){
		x=xx;
		y=yy;
	}
	Point(Point &p);
	int getX(){return x;}
	int getY(){return y;}
private:
	int x,y;
};


Point::Point(Point&p){
    x=p.x;
    y=p.y;
    cout<<"Caling the copy constructor"<<endl;
}

复制构造函数在以下三种情况下会被调用

  • 当用类的一个对象去初始化类的另外一个对象时

    int main() {
    	Point a(1, 2); 
    	Point b(a);	//用对象a初始化对象b,复制构造函数被调用
        Point c=a;	//用对象a初始化对象c,复制构造函数被调用
    	cout << b.getX() << endl;
    	return 0;
    }
    
  • 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时

    void f(Point P){
    	cout<<p.getX()<<endl;
    }
    int main(){
        Point a(1,2);
        f(a);函数的形参为类的对象,当调用函数时复制构造函数被调用
        return 0;
    }
    只有把对象用值传递时才会调用复制构造函数,如果用传递引用,则不回调用复制构造函数。所以传递比较大的对象时,传递引用比传递值效率高的多。
    
  • 如果函数的返回值是类的对象,函数执行完成返回调用者时。

    Point g(){
    	Point a(1,2);
    	return a;//函数返回值是类的对象,返回函数值时,调用复制构造函数。
    }
    int main(){
        Point b;
        b=g();
        return 0;
    }
    

    为什么返回值是类的对象时会调用复制构造函数呢,表面上函数g将a返回给主函数,但是 a是g的局部对象,离开建立它的函数g以后就消亡了,不可能在返回主函数以后继续生存,所以在这种情况下编译系统在主函数中创建一个无名临时对象,该临时对象的生命周期只在函数调用所处的表达式中,也就是b=g( )中。执行return a时,实际上是调用复制构造函数将a的值复制给临时对象中。g运行结束时a消失,但是临时对象会存在于表达式"b=g( )"中。计算完这个表达式后,临时对象的使命结束,该临时对象就会自动消失。

默认的复制构造函数只能是浅复制

4.3.3析构函数

任何事情扫尾工作都是必须的。

析构函数的作用和构造函数刚好相反,用来完成对象被删除前的一些清理工作。

析构函数:在对象的生存期即将结束时自动调用,调用完成以后,对象就消失了,相应的内存空间被释放。//自动调用

与构造函数一样,析构函数通常是类的一个公有函数成员它的名称是类名前面加“ ~ ”构成, 没有返回值。

和构造函数不同的是析构函数不接受任何参数,但可以是虚函数(第八章)

如果不显示说明,系统也会生成一个函数体为空的隐含析构函数。

4.4类的组合

面向对象的程序设计,可以对复杂对象进行分解抽象,一个复杂对象分解为简单对象的组合,由比较容易理解和实现的部件对象装配而成。

4.4.1组合

类的组合描述的是一个类内嵌于其他类的对象作为成员的情况,它们直接的关系是包含与被包含的关系、

**当创建类的对象时,如果类具有内嵌的对象成员,那么各个内嵌对象将首先被自动创建。**因此创建对象时,既要对本类的基本类型数据成员初始化,还要对内嵌的对象成员进行初始化,这时理解对象构造函数的调用顺序很重要。

组合类构造函数的定义一般形式:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),内嵌对象3(形参表),...
{类的初始化}
其中,“内嵌对象1(形参表),内嵌对象2(形参表),内嵌对象3(形参表),...”称为初始化列表作用是对内嵌对象初始化

对基本类型的数据成员也可以初始化

在创建一个组合类的对象时,不仅它自身的构造函数的函数体将被执行,而且还将调用其内嵌对象的构造函数。这时构造函数的调用顺序如下:

  • (1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序。注意,内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
  • (2)执行本类构造函数的函数体。
//组合类的复制构造函数
Line::Line(Line &l):p1(l.p1),p2(l.p2)        //内嵌对象初始化,调用复制构造函数(2) 
{
	cout<<"这是Line的复制构造器"<<endl;
	len=l.len; 
 } 

有些数据成员的初始化必须在构造函数的初始化列表中进行

1.没有默认构造函数的内嵌对象

2.引用类型的数据成员

这两类成员编译器都不能够为其提供隐含的默认构造函数,这时就必须编写显式的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员初始化

class Point {
public:
	Point(int xx = 0, int yy = 0) {
		x = xx;
		y = yy;
	}
	Point(Point& p);
	int getX() {
		return x;
	}
	int getY() {
		return y;
	}
private:
	int x,  y;
};
Point::Point(Point& p) {
	x = p.x;
	y = p.y;
	cout << "Calling the copy constructor of Point" << endl;
}
//类的组合
class Line {
public:
	Line(Point xp1, Point xp2);
	Line(Line& l);
	double getLen() { return len; }
private:
	Point p1, p2;//Point 类的两个对象
	double len;
};
//组合类的构造函数
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
	cout << "Calling constructor of Line" << endl;
	double x = static_cast<double>(p1.getX() - p2.getX());
	double y = static_cast<double>(p1.getY() - p2.getY());
	len = sqrt(x * x + y * y);
}
//组合类的复制构造函数
Line::Line(Line& l) :p1(l.p1), p2(l. p2) {//l.p1值复制给p1
	cout << "Calling the copy constructor of line" << endl;
	len = l.len;
}
int main() {
	Point myp1(1, 1), myp2(4, 5);//建立Point类对象
	Line line(myp1, myp2);//建立Line类对象,形参是类的对象
	Line line2(line);//利用复制构造函数建立一个新对象
	cout << "the length of the line is:";
	cout << line.getLen() << endl;
	cout << "The length of the line2 is ";
	cout << line2.getLen() << endl;
	return 0;
}
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling constructor of Line
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of line
the length of the line is:5
The length of the line2 is 5
//6次拷贝构造
    Line line(myp1, myp2);
中对象是函数的参数,所以在函数参数形参结合时需要调用Point类的复制构造函数,即1、2两次
    而在Line::Line(Point xp1, Point xp2): p1(xp1), p2(xp2)
中因为要采用类对Line中的内嵌对象进行初始化,即,用xp1对p1进行初始化,用xp2对p2进行初始化。所以在此又调用了两次,即3、4

4.4.2前向引用声明

C++的类应当先定义,然后再使用。但是在处理相对复杂的问题,考虑类 的组合时,很可能遇到两个类相互引用 的情况,这种情况称为循环依赖。

class A{        //A的定义
pubilc:         //外部接口
    void f(B b);//以B类对象b为形参的成员函数
    			//这里编译错误,因为"B"是非法符号
};
class B{        //B的定义
public:         //外部接口
    void g(A a);//以A类对象a为形参的成员函数
};
这里,A的公有成员函数f的形式参数是类B的对象同时B的公有成员函数g也以类A的对象为形参。
使用一个类之前首先要定义该类所以会出错。

前向引用声明:在引用未定义的类之前,将该类的名字告诉编译器,使得编译器知道那是一个类名

Class B;        //前向引用声明
class A{        //A的定义
pubilc:         //外部接口
    void f(B b);//以B类对象b为形参的成员函数
};
class B{        //B的定义
public:         //外部接口
    void g(A a);//以A类对象a为形参的成员函数
};

前向引用声名,在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联函数种使用该类 的对象。

class fred;
class barney{
fred x;         //错误:类fred的定义尚不完善不能定义fred的对象
};
class fred{
barney y;
};
对类的前向引用声明只能说明Fred是一个类名,而不能给出该类的完整定义,因此在类Barney中就不能定义类Fred的数据成员。
class fred;
class barney{
pubilc:
    ...
    void method(){//内联函数
         x.hanhsu();//错误:fred类对象在定义之前被使用(hanshu()未定义)
    }
private:
fred &x;            //正确:前向引用后,可以声明fred类的对象引用或指针
}
class fred{
pubilc:
    ...
    void hashu();
private:
    barney &y;
}
编译时指出错误,因为在类Barney的内联函数中使用了有x所指向的、Fred类的对象,而此时Fred类尚未被完整的定义。

4.5 UML图形标识

4.5.1 UML简介

UML简介: UML是一种典型的面向对象的建模语言,不是一种编程语言,在UML语言种用符号描述概念,概念间的关系描述为连接符号的线。

4.5.2 UML类图

4.6结构体和联合体

4.6.1结构体

结构体是一种特殊形态的类,它和类一样有自己的数据成员和函数成员,可以有自己的构造函数和析构函数。

结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性:类中未指明的访问控制属性为私有类型,在结构体中未指明的访问控制属性为公有属性。

pubilc: //外部接口
void f(B b);//以B类对象b为形参的成员函数
};
class B{ //B的定义
public: //外部接口
void g(A a);//以A类对象a为形参的成员函数
};


前向引用声名,在**提供一个完整的类定义之前,不能定义该类的对象**,也不能在内联函数种使用该类 的对象。

```C++
class fred;
class barney{
fred x;         //错误:类fred的定义尚不完善不能定义fred的对象
};
class fred{
barney y;
};
对类的前向引用声明只能说明Fred是一个类名,而不能给出该类的完整定义,因此在类Barney中就不能定义类Fred的数据成员。
class fred;
class barney{
pubilc:
    ...
    void method(){//内联函数
         x.hanhsu();//错误:fred类对象在定义之前被使用(hanshu()未定义)
    }
private:
fred &x;            //正确:前向引用后,可以声明fred类的对象引用或指针
}
class fred{
pubilc:
    ...
    void hashu();
private:
    barney &y;
}
编译时指出错误,因为在类Barney的内联函数中使用了有x所指向的、Fred类的对象,而此时Fred类尚未被完整的定义。

4.5 UML图形标识

4.5.1 UML简介

UML简介: UML是一种典型的面向对象的建模语言,不是一种编程语言,在UML语言种用符号描述概念,概念间的关系描述为连接符号的线。

4.5.2 UML类图

4.6结构体和联合体

4.6.1结构体

结构体是一种特殊形态的类,它和类一样有自己的数据成员和函数成员,可以有自己的构造函数和析构函数。

结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性:类中未指明的访问控制属性为私有类型,在结构体中未指明的访问控制属性为公有属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值