从C语言快速过渡到C++
帮助学会C语言的同学快速过渡到C++
1. C++概述
1.1 C++简介
“C++”中的++来自于C语言中的递增运算符++,该运算符将变量加1。C++起初也叫“c with clsss”。通过名称表明,C++是对C的扩展,因此C++是C语言的超集,这意味着任何有效的C程序都是有效的C++程序。C++程序可以使用已有的c程序库。
库是编程模块的集合,可以在程序中调用它们。库对很多常见的编程问题提供了可靠的解决方法,因此可以节省程序员大量的时间和工作量
C++语言在C语言的基础上添加了面向对象编程和泛型编程的支持。C++继承了C语言高效,简洁,快速和可移植的传统。
C++融合了3种不同的编程方式:
- C语言代表的过程性语言。
- C++在C语言基础上添加的类代表的面向对象语言。
- C++模板支持的泛型编程。
*c语言和c++语言的关系:*
c++语言是在C语言的基础上,添加了面向对象、模板等现代程序设计语言的特性而发展起来的。两者无论是从语法规则上,还是从运算符的数量和使用上,都非常相似,所以我们常常将这两门语言统称为“C/C++”。 C语言和C++并不是对立的竞争关系:
1)C++是C语言的加强,是一种更好的C语言。
2)C++是以C语言为基础的,并且完全兼容C语言的特性。C语言和C++语言的学习是可以相互促进。学好C语言,可以为我们将来进一步地学习C++语言打好基础,而C++语言的学习,也会促进我们对于C语言的理解,从而更好地运用C语言。
1.2 c++起源
与c语言一样,c++也是在贝尔实验室诞生的,Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)在20世纪80年代在这里开发了这种语言。
(C++之父-本贾尼·斯特劳斯特卢普)
Stroustrup关心的是让C++更有用,而不是实施特定的编程原理或风格。在确定语言特性方面,真正的编程比纯粹的原理更重要。Stroustrup之所以在C的基础上创建C++,是因为C语言简洁、适合系统编程、使用广泛且与UNIX操作系统联系紧密。
用他自己的话来说,“C++主要是为了我的朋友和我不必再使用汇编语言、C语言或者其他现代高级语言来编程而设计的。它的主要功能是可以更方便得编写出好程序,让每个程序员更加快乐”。
1.3 可移植性和标准
假设为运行windows 2000的老式奔腾pc编写了一个很好用的c++程序,而管理员决定使用不同操作系统(比如说Mac OS 或 Linux)和处理器的计算机替换它。该程序是否可在新平台运行呢?当然,但是必须使用为新平台设计的c++编译器重新编译。但是是否需要修改写好的代码?如果不需要修改代码的情况下,重新编译程序后,程序依然运行良好,该程序是可移植的。
程序是否可移植性有两个问题需要解决。第一是硬件,针对特定硬件编程的程序是不可移植的。第二,语言的实现,windows xp c++ 和 Redhat Linux 或 Mac OS X对c++的实现不一定相同。虽然我们希望c++版本与其他版本兼容,但是如果没有一个公开的标准,很难做到。因此,美国国家标准局(American National Standards Institute,ANSI)在1990年设立一个委员会专门负责制定c++标准(ANSI制定了c语言的标准)。国际标准化组织(International Organization for Standardization,ISO)很快通过自己的委员会加入到这个行列,创建了联合组织ANSI/ISO,制定c++标准。
经过多年的努力,制定出了一个国际标准ISO/IEC 14882:1998 ,并于1998年获得了ISO、IEC(International Electrotechnical Committee,国际电工技术委员会)和ANSI的批准。这个标准就是我们经常所说的c++98。它不仅描述了已有的c++特性,还对语言进行了扩展,添加了异常、运行阶段类型识别(RTTI)、模板和标准模板库(STL).
2003年,发布了c++标准第二版(IOS/IEC 14882:2003),这一版本对第一版修订了一些错误,但并没有改变语言特性,因此c++98表示c++98/c++2003.
c++不断发展。IOS标准委员会于2011年8月批准了新标准ISO/IEC 14882:2011,该标准被称为c++11,与c++98一样c++11也新增了许多特性。
ISO c++标准还吸收了ANSI c语言标准,c++尽量做到是c的超集。意味着在理想情况下,任何有效的c程序都应该是有效的c++程序。
ANSI不仅定义了c语言,还 定义了一个ANSI c必须实现的标准c库。c++也在使用这个库,另外ANSI/ISO c++标准还提供了一个c++标准类库。、
1.4 为什么C++会成功
c++最初的目的是将c语言转变为OOP(面向对象程序设计)语言,但是c++后来并没有这么做,而是立足于程序的实际。因为在c语言方面大量投入的程序员使其完全丢掉c语言那种编程的思考方式,转而去接受一种新的语言,新的思维,那么将会导致这些程序员中大部分人在短时间内可能毫无成果,使其生产率降低。但是如果让这些c程序员在已有知识的基础上,再去学习c++语言,理解运用OOP,那么也只是在其已有思维的基础上进行扩展而已,这样可以保持其更好的生产率。
简而言之,强迫程序员放弃c语言和c语言的思考方式,而去转到OOP上是需要代价的,但是从c语言转到c++所花费的代价就会小很多。所以也可以理解为c++的出现并不是去替代c,而是对c的扩展,所以在c++中既可以使用c++新特性,并且可以使用c的过程式思维来编写程序。
对于传统的结构化语言,我们向来没有太多的疑惑,函数调用那么自然而明显,只是从程序的某一个地点调到另一个地点去执行。但是对于面向对象(OO)语言,我们疑惑就会很多。其原因就是c++编译器为我们程序员做了太多隐藏的工作:构造函数,析构函数、虚函数、继承、多态…有时候它为我们合成出一些额外的函数,有时候它又偷偷在我们写的函数里,放进更多的操作。有时候也会给我们的对象里放进一些奇妙的东西,使得我们sizeof的时候结果可我们预期不一样。
2.C++初识
2.1 简单的C++程序
2.1.1 C++ hello world
- 引入头文件 #include 标准输入输出流
- 使用标准命名空间 using namespace std;
- 标准输出流对象 cout << “…” << 1234 << 3.14 << endl;
- 面向对象三大特性
封装、继承、多态
代码实例
#include <iostream> // 标准输入输出流,i表示input输入,o表示output输出 stream表示流,故iostream头文件表示输入输出流
using namespace std; // 命名空间 using--使用 std--标准 namespace--命名空间,注意分号不能省略
int main()
{
// cout 标准输出流对象
// << 左移 在C++有了新的寓意, 用以在count后拼接输出的内容
// endl -- endline 刷新缓冲区,并且换行
cout << "Hello World" << endl;
return 0;
}
运行结果:
Hello World
问题1:C++头文件为什么没有.h?
在C语言中头文件使用扩展名.h,将其作为一种通过名称标识文件类型的简单方式。但是C++得用法改变了,C++头文件没有扩展名。但是有些C语言的头文件被转换为C++的头文件,这些文件被重新命名,丢掉了扩展名.h(使之成为C++风格头文件),并在文件名称前面加上前缀c(表明来自C语言)。例如C++版本的math.h为cmath.
由于C使用不同的扩展名来表示不同文件类型,因此用一些特殊的扩展名(如hpp或hxx)表示c++的头文件也是可以的,ANSI/IOS标准委员会也认为是可以的,但是关键问题是用哪个比较好,最后一致同意不适用任何扩展名。
头文件类型 | 约定 | 示例 | 说明 |
---|---|---|---|
c++旧式风格 | 以.h结尾 | iostream.h | c++程序可用 |
c旧式风格 | 以.h结尾 | math.h | c/c++程序可用 |
c++新式风格 | 无扩展名 | iostream | c++程序可用,使用namespace std |
转换后的c | 加上前缀c,无扩展名 | cmath | c++程序可用,可使用非c特性,如namespace std |
问题2:using namespace std 是什么?
namespace是指标识符的各种可见范围。命名空间用关键字namespace 来定义。命名空间是C++的一种机制,用来把单个标识符下的大量有逻辑联系的程序实体组合到一起。此标识符作为此组群的名字。
问题3:cout 、endl 是什么?
cout是c++中的标准输出流,endl是输出换行并刷新缓冲区。
2.1.3 面向过程
面向过程是一种以过程为中心的编程思想。
通过分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向过程编程思想的核心:功能分解,自顶向下,逐层细化(程序=数据结构+算法)。
面向过程编程语言存在的主要缺点是不符合人的思维习惯,而是要用计算机的思维方式去处理问题,而且面向过程编程语言重用性低,维护困难。
2.1.4 面向对象
面向对象编程(Object-Oriented Programming)简称 OOP 技术,是开发计算机应用程序的一种新方法、新思想。过去的面向过程编程常常会导致所有的代码都包含在几个模块中,使程序难以阅读和维护。在做一些修改时常常牵一动百,使以后的开发和维护难以为继。而使用 OOP 技术,常常要使用许多代码模块,每个模块都只提供特定的功能,它们是彼此独立的,这样就增大了代码重用的几率,更加有利于软件的开发、维护和升级。
在面向对象中,算法与数据结构被看做是一个整体,称作对象,现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合一地来描述,所以可以用下面的等式来定义对象和程序:
对象 = 算法 + 数据结构程序 = 对象 + 对象 + ……
从上面的等式可以看出,程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。
面向对象编程思想的核心:应对变化,提高复用。
2.1.5 面向对象三大特性
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。
继承
继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。
继承的作用:避免公用代码的重复开发,减少代码和数据冗余。
多态
多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
3. C++对C的扩展
3.1 ::作用域运算符
- ::代表作用域,如果前面什么都不添加代表全局作用域。
通常情况下,如果有两个同名变量,一个是全局变量,另一个是局部变量,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
代码实例
#include <iostream>
using namespace std;
//全局变量
int a = 10;
void test()
{
//局部变量
int a = 20;
cout << "a:" << a << endl;
}
int main()
{
test();
return 0;
}
运行结果
20
在test的输出语句语句中,使用的变量a是test函数内定义的局部变量美因茨输出的结果为局部变量的a值。
作用域运算符可以解决局部变量与全局变量的重名问题。
代码
#include <iostream>
using namespace std;
//全局变量
int a = 10;
void test()
{
//局部变量
int a = 20;
cout << "打印局部变量" << endl;
cout << "a:" << a << endl;
cout << "打印全局变量" << endl;
cout << "::a:" << ::a << endl;
}
int main()
{
test();
return 0;
}
运行结果
打印局部变量
a:20
打印全局变量
::a:10
这个例子可以看出,作用域运算符可以用来解决局部变量与全局变量的重名问题,即在局部变量的作用域内,可用::对被屏蔽的同名的全局变量进行访问。
3.2 名字控制
创建名字是程序设计过程中一项最基本的活动,当一个项目很大时,它会不可避免地包含大量名字。c++允许我们对名字的产生和名字的可见性进行控制。
我们之前在学习c语言可以通过static关键字来使得名字只得在本编译单元内可见,在c++中我们将通过一种通过命名空间来控制对名字的访问。
3.2.1 C++命名空间(namespace)
在c++中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称互相冲突性的可能性越大。另外使用多个厂商的类库时,也可能导致名称冲突。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入关键字namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。
3.2.2 命名空间的语法
- 命名空间用途:解决名称冲突,例如调用一些库时,会存在重名的问题,引入命名空间很好的解决这个问题
- 命名空间下可以存放 : 变量、函数、结构体、类…
- 命名空间必须要声明在全局作用域
- 命名空间可以嵌套命名空
- 命名空间是开放的,可以随时将新成员添加到命名空间下
- 命名空间可以匿名的-----------定义在全局变量,且只能在本文件使用–等价于批量定义static
- 命名空间可以起别名
- 命名空间内函数可实现声明和定义分离
上述性质通过代码来进行演示。
创建多个命名空间,解决名称冲突。
namespace A
{
int a = 10;
}
namespace B
{
int a = 20;
}
void print()
{
cout << "A::a = " << A::a << endl; //打印命名空间A的变量a
cout << "B::A = " << B::a << endl; //打印命名空间B的变量a
}
int main()
{
print();
return 0;
}
命名空间只能全局范围内定义(以下错误写法)
void test(){
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}
很明显,命名空间下A,B的在test函数内定义,出现错误。
命名空间可嵌套命名空间
#include <iostream>
using namespace std;
namespace A{
int a = 10;
namespace B{
int a = 20;
}
}
void test()
{
cout << "A::a = " << A::a << endl;
cout << "A::B::a = " << A::B::a << endl;
}
int main()
{
test();
return 0;
}
命名空间是开放的,即可以随时把新的成员加入已有的命名空间中
namespace A
{
int a = 10;
}
namespace A
{
void func()
{
cout << "hello namespace" << endl;
}
}
void test()
{
cout << "A::a: " << A::a << endl;
A::func();
}
int main()
{
test();
return 0;
}
声明和实现可分离
namespace MySpace
{
void func1();
void func2(int param);
}
void MySpace::func1()
{
cout << "MySpace::func1" << endl;
}
void MySpace::func2(int param)
{
cout << "MySpace" << param << endl;
}
命名空间别名
namespace veryLongName
{
int a = 10;
void func(){ cout << "hello namespace" << endl;}
}
void test()
{
namespace shortName = veryLongName; // 进行改名,将要修改的名=原来的名称
cout << "veryLongName::a:" << shortName::a << endl;
veryLongName::func();
shortName::func();
}
int main()
{
test();
return 0;
}
3.2.3 using声明
using声明可使得指定的标识符可用。
若using声明的标识符与其他的标识符名字相同,以using声明的标识符为准,详请:代码中3.同名冲突。
namespace A{
int paramA = 10;
int paramB = 30;
void funcA() {cout << "hello funcA" << endl; }
void funcB() {cout << "hello funcA" << endl; }
}
void test()
{
//1.通过命名空间域运算符
cout << A::paramA << endl;
//2.using声明
using A::paramA;
using A::funcA;
cout << paramA << endl; // 可以直接访问,因为使用了using A::paramA
// cout << paramB << endl; // 不可以直接访问,因为没有使用using声明
funcA();
//3.同名冲突
// int parmaA = 20; //这里即便定义了parmaA,也会被A::paramA所覆盖。这里的定义无效
}
int main()
{
test();
return 0;
}
using声明碰到函数重载
namespace A{
void func() {}
void func(int x) {}
void func(int x, int y) {}
}
int main()
{
using A::func;
func();
func(10);
func(10, 20);
return 0;
}
如果命名空间包含一组用相同名字重载的函数,using声明就声明了这个重载函数的所有集合。
3.2.4 using编译指令
using编译指令使整个命名空间标识符可用。
- 如果using编译指令与就近原则冲突时,以就近原则为准。
- 若两个using编译指令中标识符冲突时,发生报错。
#include <iostream>
using namespace std;
namespace A{
int paramA = 20;
int paramB = 30;
void funcA() { cout << "hello funcA" << endl;}
void funcB() { cout << "hello funcB" << endl;}
}
void test01()
{
using namespace A;
cout << paramA << endl;
cout << paramB << endl;
funcA();
funcB();
//不会产生二义性
int parmaA = 30;
cout << parmaA << endl; // 这里的输出为parmaA,满足就近原则
}
namespace B
{
int paramA = 20;
int paramB = 30;
void funcA() { cout << "hello funcA" << endl;}
void funcB() { cout << "hello funcB" << endl;}
}
void test02()
{
using namespace A;
using namespace B;
//二义性产生,不知道是调用A还是B的paramA
//cout << paramA << endl; //发生报错
}
int main()
{
test01();
test02();
return 0;
}
详细分析using声明和using编译指令
using 声明通常是声明命名空间其中单个标识符,例如: using A::paramA;
using 编译指令是使整个命名空间标识符可用,例如: using namespace A;
当发生同名冲突的情况(标识符名称相同),会出现什么情况呢?
假设A,B为两个不同的命名空间,且拥有相同名称的标识符paramA=20
// using声明与就近规则
void test01()
{ int paramA = 10;
using A::parmaA;
// 就近规则失效,无论在声明前后定义,parmaA为A::parmaA
}
// using声明与using声明
void test02()
{
using A::parmaA;
using B::parmaA;
//using B::parmaA语句报错,parmaA为A::parmaA
}
// using声明与using编译指令
void test03()
{
using A::parmaA;
using namespace B;
// parmaA为A::parmaA
}
// using编译指令与就近规则
void test04()
{
int parmaA = 10;
using namespace A;
//就近规则触发,parmaA=10
}
//using编译指令与using编译指令
void test05()
{
using namespace A;
using namespace B;
//标识符parmaA存在二义性,关于parmaA的语句报错
}
总结来看,在重名的情况下,标识符的选用规则为:using声明>就近规则>using编译指令,若出现平行的情况会报错。
3.2.5 命名空间使用
我们刚讲的一些东西一开始会觉得难一些,这些东西以后还是挺常用,只要理解了它们的工作机理,使用它们非常简单。
需要记住的关键问题是当引入一个全局的using编译指令时,就为该文件打开了该命名空间,它不会影响任何其他的文件,所以可以在每一个实现文件中调整对命名空间的控制。比如,如果发现某一个实现文件中有太多的using指令而产生的命名冲突,就要对该文件做个简单的改变,通过明确的限定或者using声明来消除名字冲突,这样不需要修改其他的实现文件。
3.3 全局变量检测增强
C++下全局变量检测变得严格
int a;//没有赋值,当做声明
int a=10;//存在赋值,当做定义
//C下可以(有的编译器C下也不可以),C++重定义
3.4 C++中所有的变量和函数都必须有类型
- 函数的返回值(类型)
- 形参类型
- 形参个数
在C语言中,可以没有函数返回值\形参的类型,如果没有为任何类型即可。
在C++中,没有函数的返回值\形参的类型,认定为void。
举例:
在C语言中,int fun() 表示返回值为int,接受任意参数的函数,int fun(void) 表示返回值为int的无参函数。
在C++ 中,int fun() 和int fun(void) 具有相同的意义,都表示返回值为int的无参函数。
3.5 更严格的类型转换
在C++,不同类型的变量一般是不能直接赋值的,需要相应的强转。
char *p=(char *)malloc(64);
//C++下要求等号左右类型一致
3.6 struct类型加强
- C中定义结构体变量需要加上struct关键字,C++不需要。
- C中的结构体只能定义成员变量,不能定义成员函数。C++即可以定义成员变量,也可以定义成员函数。
//1.结构体中即可以定义成员变量, 也可以定义成员函数
struct Student{
string mName;
int mAge;
void setName(string name) { mName = name; }
void setAge(int age) { mAge = age; }
void showStudent(){ cout << "Name:" << mName << " Age:" <<mAge << endl;}
};
//2.C++中定义结构体变量不需要加上struct关键字
void test01()
{
Student student;
student.setName("John");
student.setAge(20);
student.showStudent();
}
int main()
{
test01();
return 0;
}
3.7 新增bool类型关键字
标准c++的bool类型有两种内建的常量**true(转换为整数1)和false(转换为整数0)**表示状态。这三个名字都是关键字。
- bool类型只有两个值,true(1值),false(0值)
- bool类型占1个字节大小
- 给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)
代码
void test01()
{
cout <<"sizeof(bool) = " << sizeof(false) << endl; // 输出结果为1,bool类型占一个字节大小
bool flag = true; // C语言中没有这种类型
flag = 100; // 给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换为false(0)
cout << "flag = " << flag << endl;
}
int main()
{
test01();
return 0;
}
运行结果
sizeof(bool) = 1
flag = 1
[C语言中的bool类型]
C语言中也有bool类型,在C99标准之前是没有bool关键字,C99标准已经有bool类型,包含头文件stdbool.h,就可以使用和C++一样的bool类型。
3.8 三目运算符功能增强
- C语言三目运算表达式返回值为数据值,为右值,不能赋值。
- C语言下,三元运算符返回值是右值(例:15)
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一个问题,(a > b ? a : b) 三目运算表达式返回的是什么?
//(a > b ? a : b) = 100;
//返回的是右值
- C++语言三目运算表达式返回值为变量本身(引用),为左值,可以赋值。
- C++下,三元运算符返回值是左值值(例:temp)
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一个问题,(a > b ? a : b) 三目运算表达式返回的是什么?
cout << "b:" << b << endl;
//返回的是左值,变量的引用
(a > b ? a : b) = 100;//返回的是左值,变量的引用
cout << "b:" << b << endl;
[左值和右值概念]
在c++中可以放在赋值操作符左边的是左值,可以放到赋值操作符右面的是右值。
有些变量即可以当左值,也可以当右值。
左值为Lvalue,L代表Location,表示内存可以寻址,可以赋值。
右值为Rvalue,R代表Read,就是可以知道它的值。
比如:int temp = 10; temp在内存中有地址,10没有,但是可以Read到它的值。
3.9 C/C++中的const
3.9.1 const概述
const单词字面意思为常数,不变的。它是C/C++中的一个关键字,是一个限定符,它用来限定一个变量不允许改变,它将一个对象转换成一个常量。
const int a = 100;
a = 1000; // 编译错误,const是一个常量,不可修改
3.9.2 C/C++中const的区别
3.9.2.1 C中的const
常量的引进是在C++早期版本中,当时标准C规范正在制定。那时,尽管C委员会决定在C中引入const,但是,他们C中的const理解为”一个不能改变的普通变量”,也就是认为const应该是一个只读变量,既然是变量那么就会给const分配内存,并且在C中const是一个全局只读变量,C语言中const修饰的只读变量是外部连接的。
如果这么写:
const int arrSize = 10;
int arr[arrSize];
看似是一件合理的编码,但是这将得出一个错误。 因为arrSize占用某块内存,所以C编译器不知道它在编译时的值是多少?
C语言下:
全局const:储存在只读数据段,但仍不能初始化数组。
//全局const
//受到常量区保护,运行修改失败
const int m_A = 100;
void test07()
{ //1.直接修改失败
//m_A = 200;
//2.间接修改语法无问题,运行报错
int *p = &m_A;
*p = 200;
printf("m_A=", m_A);
//3.int arr[m,B];在C语言下,const是伪常量,不可以初始化数组
}
局部const:不能通过直接修改,但是能通过间接修改改变,局部const变量存储在栈区。
C语言下的const是伪常量
3.9.2.1 C++中的const
在C++中,一个const不必创建内存空间,而在C中,一个const总是需要一块内存空间。在C++中,是否为const常量分配内存空间依赖于如何使用。一般说来,如果一个const仅仅用来把一个名字用一个值代替(就像使用#define一样),那么该存储局空间就不必创建。
如果存储空间没有分配内存的话,在进行完数据类型检查后,为了代码更加有效,值也许会折叠到代码中。
不过,取一个const地址, 或者把它定义为extern,则会为该const创建内存空间。
在C++中,出现在所有函数之外的const作用于整个文件(也就是说它在该文件外不可见),默认为内部连接,C++中其他的标识符一般默认为外部连接。
C++语言下:
在C++语言下,const int 可以初始化数组,且const变量无法动态分配空间。
const int a = 10;
int arr[a]; // 不报错
全局const
情况1:当使用右值来初始化全局const变量,同C语言一样,受常量区保护,储存在只读数据段,无法修改。
// 全局const变量,存储在常量区
const int a = 10;
void test01()
{
//1.直接修改---发生报错
//a = 5;
//2.间接修改---发生报错
//int *p = &a;
}
情况2:当使用左值(变量)来初始化全局const变量,全局const变量存储在栈区,故可以通过间接修改来修改成功。
int a = 10;
// 通过变量来初始化全局const变量,存储在栈区
const int b = a;
void test01()
{
//1.直接修改---发生报错
//b = 5;
//2.间接修改---修改成功
int *p =(int *)&b;
*p = 15;
cout << "b = " << b << endl;
}
情况3:当使用自定义数据类型进行全局const变量定义,定义失败,故谈不上进行修改了
struct Person
{
string m_Name;
int m_Age;
};
void test01()
{
//const Person p; //定义失败
}
总结:
- 全局const 变量使用左值定义时,直接或间接均无法修改。
- 全局const 变量使用右值定义时,直接修改失败,间接修改会成功。
局部const
情况1:用左值初始化局部const变量,局部const变量存储在符号表中,直接修改会报错,间接修改会指针会指向临时分配的内存,间接修改不发生变化。
void test01()
{ // 局部变量
const int n_B = 100;
// 直接修改会报错
// n_B = 300;
//间接修改不发生变化
int * p = (int *)&n_B;
*p = 200;
cout << "n_B = " << n_B << endl;
int arr[n_B] //可以初始化数组
}
下图的左侧是代码块,右侧是对代码块的解释:
int *p = (int *)&m_B;
//实际上,等价于如下的操作
int temp = m_B;
int *p = &temp;
//所以,使用指针p去接受m_B的地址,实际上接收的不是m_B的地址,而是一个数值与m_B相同,临时分配的地址。
情况2:用右值初始化局部const变量,局部const变量存储在栈区,直接修改失败,间接修改成功。
//使用普通变量 初始化 const变量, 存在开辟的空间
void test01()
{
int a = 10;
const int b = a;
int *p = (int *)&b; // 但若用变量初始化const,系统将会开辟内存,使得const 可以被间接修改
*p = 1000;
cout << "b = " << b << endl; // 打印为1000,间接修改成功
}
情况3:局部const变量定义自定义的数据类型,局部const变量存储在栈区,直接修改失败,间接修改成功。
#include<iostream>
#include<string>
using namespace stdl;
//对于自定义数据类型的变量,存在开辟的空间
struct Person
{
string m_Name; //可以直接将字符串赋值给类
int m_Age;
};
void test03()
{
const Person p;
//p.m_Age = 10; 直接修改不成功
Person *pp = (Person *)&p;
(*pp).m_Name = "Tom";
pp->m_Age = 10; // 间接修改成功
cout << "姓名:" << p.m_Name << " 年龄:" << p.m_Age << endl;
}
总结:
- 用左值初始化局部const变量,局部const变量存储在符号表中,直接修改会报错,间接修改会指针会指向临时分配的内存,间接修改不发生变化。
- 用右值初始化局部const变量,局部const变量存储在栈区,直接修改失败,间接修改成功。
- 局部const变量定义自定义的数据类型,局部const变量存储在栈区,直接修改失败,间接修改成功。
C++中的const使用右值来定义时,无法进行修改。
C++的const是真正的常量
3.9.2.3 const连接属性
在C语言下,const默认为外部连接属性(默认存在extern)
例如当test.c文件中出现, const int a = 10;这里默认加上了extern
在其他的.c文件中是可以使用的,需加上 extern const int a ;
在C++语言下,const默认为内部连接属性(默认不存在extern)
例如当test.cpp文件中出现, const int a = 10;其他的.cpp文件是无法使用的,需变为
extern const int a = 10;通过extern扩大作用域。
3.9.2.4C/C++中const异同总结
c语言全局const会被存储到只读数据段。c++中全局const当声明extern或者对变量取地址时,编译器会分配存储地址,变量存储在只读数据段。两个都受到了只读数据段的保护,不可修改。
const int constA = 10;
int main()
{ int* p = (int*)&constA;
*p = 200;
}
以上代码在c/c++中编译通过,在运行期,修改constA的值时,发生写入错误。原因是修改只读数据段的数据。
c语言中局部const存储在堆栈区,只是不能通过变量直接修改const只读变量的值,但是可以跳过编译器的检查,通过指针间接修改const值。
const int constA = 10;
int* p = (int*)&constA;
*p = 300;
printf("constA:%d\n",constA);
printf("*p:%d\n", *p);
运行结果:
c语言中,通过指针间接赋值修改了constA的值。
c++中对于局部的const变量要区别对待:
- 对于基础数据类型,也就是const int a = 10这种,编译器会把它放到符号表中,不分配内存,当对其取地址时,会分配内存。
const int constA = 10;
int* p = (int*)&constA;
*p = 300;
cout << "constA:" << constA << endl;
cout << "*p:" << *p << endl;
运行结果:
constA在符号表中,当我们对constA取地址,这个时候为constA分配了新的空间,*p操作的是分配的空间,而constA是从符号表获得的值。
- 对于基础数据类型,如果用一个变量初始化const变量,如果const int a = b,那么也是会给a分配内存。
int b = 10;
const int constA = b;
int* p = (int*)&constA;
*p = 300;
cout << "constA:" << constA << endl;
cout << "*p:" << *p << endl;
运行结果:
constA 分配了内存,所以我们可以修改constA内存中的值。
- 对于自定数据类型,比如类对象,那么也会分配内存。
const Person person; //未初始化age
//person.age = 50; //不可修改
Person* pPerson = (Person*)&person;
//指针间接修改
pPerson->age = 100;
cout << "pPerson->age:" << pPerson->age << endl;
pPerson->age = 200;
cout << "pPerson->age:" << pPerson->age << endl;
运行结果:
为person分配了内存,所以我们可以通过指针的间接赋值修改person对象。
c中const默认为外部连接,c++中const默认为内部连接.当c语言两个文件中都有const int a的时候,编译器会报重定义的错误。而在c++中,则不会,因为c++中的const默认是内部连接的。如果想让c++中的const具有外部连接,必须显示声明为: extern const int a = 10;
const由c++采用,并加进标准c中,尽管他们很不一样。在c中,编译器对待const如同对待变量一样,只不过带有一个特殊的标记,意思是”你不能改变我”。在c++中定义const时,编译器为它创建空间,所以如果在两个不同文件定义多个同名的const,链接器将发生链接错误。简而言之,const在c++中用的更好。
了解:能否用变量定义数组:
在支持c99标准的编译器中,可以使用变量定义数组。
- 微软官方描述vs2013编译器不支持c99.:
Microsoft C conforms to the standard for the C language as set forth in the 9899:1990 edition of the ANSI C standard.
- 以下代码在Linux GCC支持c99编译器编译通过
int a = 10;
int arr[a];
int i = 0;
for(;i<10;i++)
arr[i] = i;
i = 0;
for(;i<10;i++)
printf("%d\n",arr[i]);
3.9.3 尽量以const替换#define
在旧版本C中,如果想建立一个常量,必须使用预处理器
#define MAX 1024;
我们定义的宏MAX从未被编译器看到过,因为在预处理阶段,所有的MAX已经被替换为了1024,于是MAX并没有将其加入到符号表中。但我们使用这个常量获得一个编译错误信息时,可能会带来一些困惑,因为这个信息可能会提到1024,但是并没有提到MAX。如果MAX被定义在一个不是你写的头文件中,你可能并不知道1024代表什么,也许解决这个问题要花费很长时间。
解决办法就是用一个常量替换上面的宏。
const int max= 1024;
const和#define区别总结:
1.const有类型,可进行编译器类型安全检查。#define无类型,不可进行类型检查.
2.const有作用域,而#define不重视作用域,默认定义处到文件结尾.如果定义在指定作用域下有效的常量,那么#define就不能用。
1.宏常量没有类型,所以调用了int类型重载的函数。const有类型,所以调用希望的short类型函数?
#define PARAM 128
const short param = 128;
void func(short a)
{
cout << "short!" << endl;
}
void func(int a)
{
cout << "int" << endl;
}
int main()
{ cout << "PARAM的func函数调用" << endl;
func(PARAM);//触发int
cout << "PARAM的func函数调用" << endl;
cout << "const short的func函数调用" << endl;
func(param);//触发short
cout << "const short 的func函数调用" << endl;
return 0;
}
通过以上代码可知,宏定义是没有类型的,默认触发int类型重载函数。
- 宏常量不重视作用域.
void func1()
{ //在func1中定义了const int a为局部变量,作用域在func1的函数内部
//同时,定义了宏常量,宏常量的作用域从定义到文件结束或者到#undef
const int a = 10;
#define A 20
//#undef A // 卸载宏常量A
}
void func2()
{
//cout << "a:" << a << endl; //不可访问,超出了const int a的作用域
cout << "A:" << A << endl; // #define 作用域从定义到文件结束或者到#undef,可访问
// 总结:
//const int a在函数内定义为局部变量,在函数外定义为全局变量
//#define A 不论在哪里定义都是,作用域都是从定义开始的地方到#undef A结束
//如果#undef A不存在,则A的作用域为是从定义开始的地方到运行结束。
}
问题: 宏常量可以有命名空间吗?
namespace MySpace
{
#define num 1024
}
void test()
{ // int num = 10; // 命名错误
cout << num << endl; // 可以完成输出,对于不同的变量,MySpace::num才能输出,可见命名空间对于宏定义不会有任何的作用
}
3.10 引用(reference)
3.10.1 引用基本用法
引用是c++对c的重要扩充。在C/C++中指针的作用基本都是一样的,但是C++增加了另外一种给函数传递地址的途径,这就是按引用传递(pass-by-reference),它也存在于其他一些编程语言中,并不是C++的发明。
- 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
- 程序中通过变量来申请并命名内存空间
- 通过变量的名字可以使用存储空间
对一段连续的内存空间只能取一个别名吗?
C++中新增了引用的概念,引用可以作为一个已定义变量的别名。
基本语法:
Type& ref = val;
注意事项:
- &在此不是求地址运算,而是起标识作用。
- 类型标识符是指目标变量的类型
- 必须在声明引用变量时进行初始化。
- 引用初始化之后不能改变。
- 不能有NULL引用。必须确保引用是和一块合法的存储单元关联。
- 可以建立对数组的引用。
引用的初步使用
//认识引用
//引用的语法格式: 类型& 别名 = 原名
void test01()
{
int a = 10;
//给变量a取一个别名b
int &b = a;
cout << "a:" << a << endl; // 10
cout << "b:" << b << endl; // 10
//操作b就相当于操作a本身
b = 100;
cout << "a:" << a << endl; // 100
cout << "b:" << b << endl; // 100
cout << "-------------" << endl;
//一个变量可以有n个别名
int & c = a;
c = 200;
cout << "a:" << a << endl; // 200
cout << "b:" << b << endl; // 200
cout << "c:" << c << endl; // 200
cout << "------------" << endl;
//a, b, c的地址都是相同的
cout << "a:" << &a << endl;
cout << "b:" << &b << endl;
cout << "c:" << &c << endl;
}
运行结果
a:10
b:10
a:100
b:100
-------------
a:200
b:200
c:200
------------
a:0x7ffd6988527c
b:0x7ffd6988527c
c:0x7ffd6988527c
引用数组
// 建立对数组的引用
void test01()
{
//1.直接建立引用
int arr[10];
int (&pArr)[10] = arr;
//对直接建立引用进行验证
for(int i = 0; i < 10; i++)
{
arr[i] = 0 + i;
}
for(int i = 0; i < 10; i++)
{
cout << pArr[i] << endl;
}
cout << "=================" << endl;
//2.通过typedef建立引用
typedef int(ARRAY_TYPE)[10];
//类型 &别名 = 原名
ARRAY_TYPE & pArr2 = arr;
for(int i = 0; i < 10; i++)
{
cout << pArr2[i] << endl;
}
}
3.10.2 函数中的引用
最常见看见引用的地方是在函数参数和返回值中。当引用被用作函数参数的时,在函数内对任何引用的修改,将对还函数外的参数产生改变。当然,可以通过传递一个指针来做相同的事情,但引用具有更清晰的语法。
如果从函数中返回一个引用,必须像从函数中返回一个指针一样对待。当函数返回值时,引用关联的内存一定要存在。
// 值传递
void ValueSwap(int m , int n)
{
int temp = m;
m = n;
n = temp;
}
// 地址传递
void PointerSwap(int *m, int *n)
{
int temp = *m;
*m = *n;
*n = temp;
}
//引用传递
void ReferenceSwap(int& m, int& n)
{
int temp = m;
m = n;
n = temp;
}
void test()
{
int a = 10;
int b = 20;
// 值传递
ValueSwap(a, b);//值传递交换失败
cout << "a:" << a << "b:" << b << endl;
//地址传递
PointerSwap(&a, &b);//地址传递交换成功
cout << "a:" << a << "b:" << b << endl;
//引用传递
ReferenceSwap(a, b);//引用传递交换成功
cout << "a:" << a << "b:" << b << endl;
}
通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单:
-
函数调用时传递的实参不必加**“&”**符
-
在被调函数中不必在参数前加**“*”**符
引用作为其它变量的别名而存在,因此在一些场合可以代替指针。C++主张用引用传递取代地址传递的方式,因为引用语法容易且不易出错。
- 不能返回局部变量的引用。
- 函数当左值,必须返回引用。
using namespace std;
//返回局部变量引用
int& TestFun01()
{
int a = 10; // 局部变量
return a; // 发生报错,不能返回局部变量的引用。因为当函数执行完,局部变量也消失,空间释放
}
//返回静态变量引用
int& TestFunc02()
{
static int a = 20;// 静态局部变量,静态局部变量从程序开始执行就已经创建,等程序结束后内存释放,故可以实现返回静态变量的引用
cout << "static int a : " << a << endl;
return a;
}
int main()
{ //不能返回局部变量的引用
int& ret01 = TestFunc01();
//如果函数做左值,那么必须返回引用
TestFunc02();
TestFunc02() = 100;
return 0;
}
3.10.3 引用的本质
引用的本质在c++内部实现是一个指针常量
Type& ref = val; // Type* const ref = &val;
C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同,只是这个过程是编译器内部实现,用户不可见。
#include<iostream>
#include<string>
using namespace std;
//发现是引用,转换为int* const ref = &a;
void testFunc(int& ref)
{ //ref是引用,转换为*ref=100
ref = 100;
}
int main()
{
int a = 10;
int& aRef = a; // 自动转换为 int* const aRef = &a;这也能说明引用为什么必须初始化
aRef = 20; // 内部发现aRef是引用,自动帮我们转换为: *aRef = 20;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
testFunc(a);
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
return 0;
}
3.10.4 指针引用
在C语言中如果想改变一个指针的指向而不是它所指向的内容,函数声明可能这样:
void fun(int**);
给指针变量取一个别名。
Type* pointer = NULL;
Type*& 别名= pointer;
代码
struct Teacher
{
int mAge;
};
//指针间接修改teacher的年龄
void AllocateAndInitByPointer(Teacher** teacher)
{
*teacher = (Teacher *)malloc(sizeof(Teacher));
(*teacher)->mAge = 200;
}
//引用修改teacher年龄
void AllocateAndInitByReference(Teacher*& teacher) // 引用Teacher * 类型(引用Teacher指针类型)
{
teacher->mAge = 300;
}
void test()
{
//创建Teacher
Teacher* teacher = NULL;
//指针间接赋值
AllocateAndInitByPointer(&teacher);
cout << "AllocateAndInitByPointer:" << teacher->mAge << endl; // 200
//引用赋值,将teacher本身传到ChangeAgeByReference函数中
AllocateAndInitByReference(teacher);
cout << "AllocateAndInitByReference:" << teacher->mAge << endl; // 300
free(teacher);
}
可以用指针引用来代替二级指针。
3.10.5 常量引用
常量引用的定义格式:
const Type& ref = val;
常量引用注意:
- 字面量不能赋给引用,但是可以赋给const引用
- const修饰的引用,不能修改。
void test01()
{
int a = 100;
const int& aRef = a; //此时aRef就是a
//aRef = 200; 发生报错
a = 200;//修改成功
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
}
void test02()
{
//不能把一个字面量赋给引用
//int& ref = 100;
//但是可以吧一个字面量赋给常引用
const int& ref = 100; // int temp = 200; const int& ret = temp;
}
[const引用使用场景]
常量引用主要用在函数的形参,尤其是类的拷贝/复制构造函数。
将函数的形参定义为常量引用的好处:
- 引用不产生新的变量,减少形参与实参传递时的开销。
- 由于引用可能导致实参随形参改变而改变,将其定义为常量引用可以消除这种副作用。如果希望实参随着形参的改变而改变,那么使用一般的引用,如果不希望实参随着形参改变,那么使用常引用。
//const int& param防止函数中意外修改数据
void ShowVal(const int& param){
cout << "param:" << param << endl;
}
//假设传入int 变量a
ShowVal(a);
//通过a可以进行修改
//通过param不能修改
3.11 练习作业
- 设计一个类,求圆的周长。
2.设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
注意定义类别忘记public,
class 一定要写上访问的属性
第一题:
class Circle
{
public: // 权限,这里涉及为公共权限
double r;
void set_r(double a)
{
r = a;
}
double return_length()
{
return PI*r*2;
}
};
int main()
{
Circle a;
a.set_r(6);
cout << "圆a的周长为:" << a.return_length() << endl;
return 0;
}
第二题
class Student
{
public:
string Name;
string ID;
void set_Name(string name)
{
Name = name;
}
void set_ID(string id)
{
ID = id;
}
};
int main()
{ Student A;
A.set_Name("李明");
A.set_ID("0717");
cout << "姓名: " << A.Name <<endl;
cout << "学号: " << A.ID <<endl;
return 0;
}
3.12 内联函数(inline function)
3.12.1 内联函数的引出
C++从C中继承的一个重要特征就是效率。假如C++的效率明显低于C的效率,那么就会有很大的一批程序员不去使用C++了。
在C中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。
但是在C++出现之后,使用预处理宏会出现两个问题:
1.第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
2.第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类的成员函数。
为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,C++引入了内联函数(inline function)。
内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。
3.12.2 预处理宏的缺陷
预处理器宏存在问题的关键是我们可能认为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来是一样的,因为也容易被混淆。但是其中也会有一些微妙的问题出现:
问题一:
#define ADD(x, y) x + y
inline int Add(int x, int y)
{
return x + y;
}
void test()
{
int ret1 = ADD(10, 20) * 10; // 希望的结果是 300,ADD(10, 20)等价于10+20,故为10 + 20 * 10
int ret2 = Add(10, 20) * 10; // 希望的结果是 300,Add(10, 20)等价于(10+20),故为(10+20)*10
cout << "ret1:" << ret1 << endl; // 210
cout << "ret2:" << ret2 << endl; // 300
}
问题二:
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x, int y)
{
return x < y ? x : y;
}
void test()
{
int a = 1;
int b = 3;
cout << "COMARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x, int y):" << Compare(++a, b) << endl; // 2
}
问题三:
预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。
3.12.3 内联函数
3.12.3.1 内联函数基本概念
在C++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。
在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。
inline void func(int a);
以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:
inline int func(int a){return ++;}
注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
内联函数不需要用户手动去加,编译器会自动完成
3.12.3.2 类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。
class Person{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}
构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。
3.12.3.3 内联函数和编译器
内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?
对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。
当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。
类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。
但是C++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 不能对函数进行取址操作
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
3.13 函数的默认参数
C++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。
void TestFunc01(int a = 10, int b = 20)
{
cout << "a + b = " << a + b << endl;
}
//注意点:
//1.形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a, int b = 10, int c = 10) {}
//2.如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能在设置默认参数
void TestFunc03(int a = 0, int b = 0);
void TestFunc03(int a, int b) {}
int main()
{
//1.如果没有传参数,那么使用默认参数
TestFunc01();
//2.如果传一个参数,那么第二个参数使用默认参数
TestFunc01(100);
//3.如果传入两个参数,那么两个参数都是用我们传入的参数
TestFunc01(100, 200);
return 0;
}
3.14 函数的占位参数
C++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
void TestFunc01(int a, int b, int)
{
//函数内部无法使用占位参数
cout << "a + b = " << a + b << endl;
}
void TestFunc02(int a, int b, int = 20)
{
//函数内部依旧无法使用占位参数
cout << "a + b = " << a + b << endl;
}
int main()
{ //错误调用,占位参数也是参数,必须传参数
//TestFunc01(10, 20);
//正确调用
TestFunc01(10, 20, 30);
//正确调用
TestFunc02(10, 20);
//正确调用
TestFunc02(10, 20, 30);
return 0;
}
看起来占位参数没有什么作用,因为在函数内部无法使用占位参数,且在调用函数时还必须传参数。什么时候用,在后面我们要讲的操作符重载的后置++要用到这个。
3.15 函数重载(overload)
3.15.1 函数重载概述
能使名字方便使用,是任何程序设计语言的一个重要特征!
我们现实生活中经常会碰到一些字在不同的场景下具有不同的意思,比如汉语中的多音字“重”。
当我们说: “他好重啊,我都背不动!”我们根据上下文意思,知道“重”在此时此地表示重量的意思。
如果我们说“你怎么写了那么多重复的代码? 维护性太差了!”这个地方我们知道,“重”表示重复的意思。
同样一个字在不同的场景下具有不同的含义。那么在C++中也有一种类似的现象出现,同一个函数名在不同场景下可以具有不同的含义。
在传统c语言中,函数名必须是唯一的,程序中不允许出现同名的函数。在C++中是允许出现同名的函数,这种现象称为函数重载。
函数重载的目的就是为了方便的使用函数名。
函数重载并不复杂,等大家学完就会明白什么时候需要用到他们,以及是如何编译,链接的。
3.15.2 函数重载
3.15.2.1 函数重载基本语法
实现函数重载的条件:
相同点
- 函数处于同一个作用域
- 函数名相同
不同点(存在以下一个不同点即可)
- 参数个数不同
- 参数类型不同
- 参数顺序不同
//1.函数重载条件
//(1)参数个数不同
//(2)参数类型不同
//(3)参数顺序不同
namespace A
{
void MyFunc() { cout << "无参数" << endl; }
void MyFunc(int a) { cout << "a:" << a << endl; }
void MyFunc(string b) { cout << "b:" << b << endl;}
void MyFunc(int a, string b) { cout << "a:" << a << "b:" << b << endl;}
void MyFunc(string b, int a) { cout << "a:" << a << "b:" << b << endl;}
}
//2.返回值不作为函数重载依据
namespace B
{
void MyFunc(string b, int a) {}
//int MyFunc(string b, int a) {} //无法重载,仅按返回值区分函数无法重载
}
注意: 函数重载和默认参数一起使用,需要额外注意二义性问题的产生。
void MyFunc(string b)
{
cout << "b:" << b << endl;
}
//函数重载碰上默认参数
void MyFunc(string b, int a=10)
{
cout << "a:" << "b:" << b << endl;
}
int main()
{ //这时,两个函数都能匹配调用,产生二义性
//MyFunc("hello");
return 0;
}
思考:为什么函数返回值不作为重载条件呢?
当编译器能从上下文中确定唯一的函数的时,如int ret = func(),这个当然是没有问题的。然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,一个函数为void func(int x);另一个为int func(int x); 当我们直接调用func(10),这个时候编译器就不确定调用那个函数。所以在C++中禁止使用返回值作为重载的条件。
3.15.2.2 函数重载实现原理
编译器为了实现函数重载,也是默认为我们做了一些幕后的工作,编译器用不同的参数类型来修饰不同的函数名,比如void func(); 编译器可能会将函数名修饰成**_func
,当编译器碰到void func(int x),编译器可能将函数名修饰为_func_int
,当编译器碰到void func(int x,char c)**,编译器可能会将函数名修饰为_func_int_char我这里使用”可能”这个字眼是因为编译器如何修饰重载的函数名称并没有一个统一的标准,所以不同的编译器可能会产生不同的内部名。
void func(){}
void func(int x){}
void func(int x,char y){}
以上三个函数在linux下生成的编译之后的函数名为:
_Z4funcv // v 代表void,无参数
_Z4funci // i 代表参数为int类型
_Z4funcic // i 代表第一个参数为int类型,第二个参数为char类型
3.15.3 extern “C”浅析
以下在Linux下测试:
C函数: void MyFunc**(){} **,被编译成函数: MyFunc
C++函数: void MyFunc**(){}**,被编译成函数: _Z6Myfuncv
通过这个测试,由于C++中需要支持函数重载,所以C和C++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在C++中调用一个使用C语言编写模块中的某个函数,那么C++是根据C++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误,以上例,C++中调用MyFunc函数,在链接阶段会去找Z6Myfuncv,结果是没有找到的,因为这个MyFunc函数是C语言编写的,生成的符号是MyFunc。
那么如果我想在C++调用c的函数怎么办?
extern "C"的主要作用就是为了实现C++代码能够调用其他C语言代码。加上extern "C"后,这部分代码编译器按C语言的方式进行编译和链接,而不是按C++的方式。
MyModule.h
4. 类和对象
4.1 类和对象的基本概念
4.1.1 C和C++中struct区别
C语言struct只有变量
C++语言struct 既有变量,也有函数
4.1.2 类的封装
我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种事物组成,我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:事和物抽象到计算机程序中,也就是用程序语言表示现实的事物。
那么现在问题是如何用程序语言来表示现实事物?现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为,所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物。
比如我们要表示人这个对象,在C语言中,我们可以这么表示:
C语言:
typedef struct _Person{
char name[64];
int age;
}Person;
typedef struct _Aninal{
char name[64];
int age;
int type; //动物种类
}Ainmal;
void PersonEat(Person* person){
printf("%s在吃人吃的饭!\n",person->name);
}
void AnimalEat(Ainmal* animal){
printf("%s在吃动物吃的饭!\n", animal->name);
}
int main(){
Person person;
strcpy(person.name, "小明");
person.age = 30;
AnimalEat(&person);
return EXIT_SUCCESS;
}
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在C语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象。假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限。
C++封装理念:
1.将行为和属性作为一个整体,更好表现生活中的事物.
2.将属性的行为加以权限控制
struct与class的区别
1.struct默认为公共权限,不用对类进行访问权限的属性设置,等价于class --(public)
2.class默认为私有权限,设置时需设置访问属性
C++:
struct Person //定义人类型
{
char name[64];//人 类 属性和行为
int age;
void Person_Eat()
{
cout << name << "吃人饭" << endl;
}
};
struct Dog
{
char name[64];
int age;
void Dog_Eat()
{
cout << name << "吃狗粮" << endl;
}
};
void test()
{
struct Person p;
strcpy(p.name, "老王");
p.Person_Eat();
//p.Dog_Eat();//报错
}
封装
-
把变量(属性)和函数(操作)合成一个整体,封装在一个类中
-
对变量和函数进行访问控制
访问权限
-
在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
-
在类的外部(作用域范围外),访问权限才有意义:public,private,protected
-
在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时,private和protected是同等级的,外部不允许访问
//封装两层含义
//1.属性和行为合成一个整体
//2.访问控制, 现实事物本身有些属性和行为是不对外开放
class Person
{
//人具有的行为(函数)
public:
void Dese() { cout << "我有钱" << endl;}
//人的属性(变量)
public:
int mTall; //多高,可以让外人知道
protected:
int mMoney; //有多少钱,只能儿子孙子知道
private:
int mAge; //年龄,不想让所有人都知道
};
int main()
{
Person p;
p.mTall = 220;
//p.mMoney 保护成员外汇无法访问
//p.mAge 私有成员外部无法访问
return 0;
}
[struct和class的区别?]
class默认访问权限为private,struct默认访问权限为public.
class A{
int mAge;
};
struct B{
int mAge;
};
void test(){
A a;
B b;
//a.mAge; //无法访问私有成员
b.mAge; //可正常外部访问
}
4.1.3 将成员变量设置为private
- 可赋予客户端访问数据的一致性。
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
- 可细微划分访问控制。
使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。
设置成私有的好处:
1.可以控制可读可写
2.可以增加限制条件,有效性验证(在获取函数加入if判断)
class Person
{
private:
string m_Name; // 姓名 --- 可读可写
int m_Age = 18; // 年龄 --- 只读
string m_Lover; // 情人 --- 只写
//由于函数为public公共权限,故可以通过成员函数来修改设置属性
public:
// 设置姓名
void set_name(string a)
{
m_Name = a;
}
// 读取姓名
string get_name()
{
return m_Name;
}
// 获取年龄
int get_age()
{
return m_Age;
}
// 写入情人
void set_lover(string a)
{
m_Lover = a;
}
};
void test()
{
Person Tom;
Tom.set_name("Tom");
cout << "Tom的年龄为:" << Tom.get_age() << endl;
cout << "Tom的姓名为:" << Tom.get_name() << endl;
}
4.1.4 课堂练习
请设计一个Person类,Person类具有name和age属性,提供初始化函数(Init),并提供对name和age的读写函数(set,get),但必须确保age的赋值在有效范围内(0-100),超出有效范围,则拒绝赋值,并提供方法输出姓名和年龄。
class Person
{
private:
string Name;
int Age;
public:
//设置名称
void set_name(string name)
{
Name = name;
}
//获取名称
string get_name()
{
return Name;
}
int set_age(int age)
{
if((age<=0)||(age>=100))
{
cout << "年龄错误!赋值失败!" << endl;
return 0;
}
Age = age;
return 1;
}
int get_age()
{
return Age;
}
};
void test()
{
Person person;
person.set_age(18);
person.set_name("Li Ming");
cout << "年龄: " << person.get_age() << endl;
cout << "姓名: " << person.get_name() << endl;
}
4.2 面向对象程序设计案例
4.2.1 设计立方体类
设计立方体类(Cube),求出立方体的面积( 2 * a * b + 2 * a * c + 2 * b * c )和体积( a * b * c),分别用全局函数和成员函数判断两个立方体是否相等。
class Cub // 立方体
{
private:
int Length; //立方体的长
int Width; //立方体的宽
int Height; //立方体的高
public:
//设置长
void set_length(int length)
{
Length = length;
}
//获取长
int get_length()
{
return Length;
}
//设置宽
void set_width(int width)
{
Width = width;
}
//获取宽
int get_width()
{
return Width;
}
//设置高
void set_height(int height)
{
Height = height;
}
//获取高
int get_height()
{
return Height;
}
//获取面积
int get_s()
{
return 2*(Length * Width + Width * Height + Length * Height);
}
//获取体积
int get_V()
{
return Length * Width * Height;
}
//比较两个立方体是否相等
bool compare(Cub& c)
{
if(get_height() == c.get_height() && get_width() == c.get_width() && get_height() == c.get_height())
{
return true;
}
return false;
}
};
bool compare(Cub &a, Cub &b)
{
if(a.get_width() == b.get_width() && a.get_height() == b.get_height() && a.get_length() == b.get_length())
{
return true;
}
else {
return false;
}
}
int main()
{
Cub cub,cub0;
//设置cub,长、宽、高
cub.set_height(2);
cub.set_width(2);
cub.set_length(2);
//设置cub0,长、宽、高
cub0.set_height(2);
cub0.set_width(2);
cub0.set_length(2);
cout << "立方体的长度:" << cub.get_length() << endl;
cout << "立方体的宽度:" << cub.get_width() << endl;
cout << "立方体的高度:" << cub.get_height() << endl;
cout << "立方体的面积:" << cub.get_s() << endl;
cout << "立方体的体积:" << cub.get_V() << endl;
if(cub.compare(cub0))
{
cout << "两个立方体相同" << endl;
}
else
{
cout << "两个立方体不相同" << endl;
}
if(compare(cub, cub0))
{
cout << "两个立方体相同" << endl;
}
else
{
cout << "两个立方体不相同" << endl;
}
return 0;
}
4.2.2 点和圆的关系
设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。
假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:
1)点在圆上:(x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) == r * r
2)点在圆内:(x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) < r * r
3)点在圆外:(x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) > r * r
//点类
class Point
{
private:
int mX;
int mY;
public:
void setX(int x) {mX = x;}
void setY(int y) {mY = y;}
int getX() {return mX;}
int getY() {return mY;}
};
//圆类
class Circle
{
private:
Point mP; // 圆心
int mR; // 半径
public:
void setP(int x, int y)
{
mP.setX(x);
mP.setY(y);
}
void setR(int r) { mR = r;}
Point& getP() {return mP;}
int getR() {return mR;}
//判断点和圆的关系
void IsPointInCircle(Point& point)
{
int distance = (point.getX() - mP.getX())*(point.getX() - mP.getX()) + (point.getY() - mP.getY())*(point.getY() - mP.getY());
int radius = mR * mR;
if(distance < radius)
{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆内!" << endl;
}
else if(distance > radius)
{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆外" << endl;
}
}
};
int main()
{ Point p;
p.setX(0);
p.setY(0);
Point p1;
p1.setX(2);
p1.setY(2);
Circle a;
a.setP(0, 0);
a.setR(1);
a.IsPointInCircle(p1);
return 0;
}
4.3 对象的构造和析构
4.3.1 初始化和清理
我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置,也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。
从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。
那么我们C++中OO思想也是来源于现实,是对现实事物的抽象模拟,具体来说,当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。C++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。
4.3.2 构造函数和析构函数
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数。
ClassName(){}
析构函数语法:
析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。
~ClassName(){}
class Person
{
public:
char* pName;
int mTall;
int mMoney;
public:
Person()
{
cout << "构造函数调用" << endl;
pName = (char *)malloc(sizeof("John"));
strcpy(pName, "John");
mTall = 150;
mMoney = 100;
}
~Person()
{
cout << "析构函数调用" << endl;
if(pName != NULL)
{
free(pName);
pName = NULL;
}
}
};
void test()
{
Person person;
cout << person.pName << person.mTall << person.mMoney << endl;
}
int main()
{
Person p;
test();
return 0;
}
4.3.3 构造函数的分类及调用
按参数类型:分为无参构造函数和有参构造函数
按类型分类:普通构造函数和拷贝构造函数(复制构造函数)
class Person
{
public:
//无参构造函数
Person()
{
cout << "no param constructor!" << endl;
mAge = 0;
}
//有参构造函数
Person(int age)
{
cout << "1 param constructor!" << endl;
mAge = age;
}
//拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象
Person(const Person& person)
{
cout << "copy constructor!" << endl;
mAge = person.mAge;
}
//打印年龄
void PrintPerson()
{
cout << "Age:" << mAge << endl;
}
private:
int mAge;
};
//1.无参构造调用方式
void test01()
{
//调用无参数构造函数
Person person1;
person1.PrintPerson();
//无参构造函数错误调用方式
//Person person2();
//person2.PrintPerson();
}
//2.调用有参构造函数
void test02()
{
//第一种 括号法,最常用(调用有参构造函数---不是拷贝构造函数)
Person person01(100);
person01.PrintPerson();
//调用拷贝构造函数
Person person02(person01);
person02.PrintPerson();
//第二种 匿名对象(显示调用有参构造函数----不是拷贝构造函数)
Person(200);// 匿名对象,没有名字的对象
Person person03 = Person(300);
person03.PrintPerson();
//注意:使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型
Person person06(Person(400));//等价于 person06 = Person(400);
person06.PrintPerson();
}
int main()
{
test01();
test02();
return 0;
}
b为A的实例化对象,A a = A(b) 和 A(b)的区别?
当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 A b。
总结:
Teacher t1;
Teacher (t1); // 等价于---Teacher t1
Teacher t2 = Teacher (t1); // 单单来讲Teacher(t1)为匿名对象,t2不是匿名对象
Teacher (100);// 为匿名对象
Teacher t1 = Teacher(100);// Teacher(100)也为匿名对象
注意:不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确:
class Teacher{
public:
Teacher(){
cout << "默认构造函数!" << endl;
}
Teacher(const Teacher& teacher){
cout << "拷贝构造函数!" << endl;
}
public:
int mAge;
};
void test(){
Teacher t1;
//error C2086:“Teacher t1”: 重定义
Teacher(t1); //此时等价于 Teacher t1;
}
4.3.4 拷贝构造函数的调用时机
- 对象以值传递的方式传给函数参数
- 函数局部对象以值传递的方式从函数返回(vs debug模式下调用一次拷贝构造,qt不调用任何构造)
- 用一个对象初始化另一个对象
//构造函数分类
//按照参数分类: 无参构造(默认构造函数) 和 有参构造
//按照类型分类: 普通构造函数 拷贝构造函数
class Person
{
public:
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int a)
{
m_Age = a;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p) //拷贝构造函数在定义时必须是引用,其他的函数不用
{
m_Age = p.m_Age;
cout << "Person的拷贝构造函数调用" << endl;
}
int m_Age;
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
// 1.用已经创建好的对象来初始化新的对象
void test01()
{
Person p1(18); //有参数构造函数
Person p2 = Person(p1); //拷贝构造函数
cout << "p2的年龄:" << p2.m_Age << endl;
}
// 2.值传递的方式 给函数参数传值
void dowork(Person p1)//这里进行拷贝赋值的操作,故为拷贝构造参数
{
}
void test02()
{
Person p1(100);//有参构造函数
dowork(p1); //拷贝构造函数
}
//3.以值方式,返回局部对象,返回值
Person dowork2()
{
Person p;//默认构造函数
return p; //不能返回&,因为p为局部变量,会消失
}
void test03()
{
Person p1 = dowork2();//拷贝构造函数 --- 在p的基础上生成了新的变量不同于p的一份
}
/*但是 在release版本下运行
void test03()将会发生改变----release编译器会做优化
(全局函数)dowork2(Person &p){}
Person p;
dowork2(p)
简单来说,release的版本会将传参拷贝变成引用,故不进行拷贝,而是让局部变量起别名指向同一块内存,然后通过dowork2去操控p
*/
[Test03结果说明:]
编译器存在一种对返回值的优化技术,RVO(Return Value Optimization)。在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数。
我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。
所以在这里,编译器偷偷帮我们做了一层优化:
当我们这样去调用: Person p = MyBusiness();
编译器偷偷将我们的代码更改为:
void MyBussiness(Person& _result)
{
_result.X:X(); //调用Person默认拷贝构造函数
//.....对_result进行处理
return;
}
int main(){
Person p; //这里只分配空间,不初始化
MyBussiness(p);
}
4.3.5 构造函数调用规则
默认情况下,C++编译器至少为我们写的类增加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
注意默认构造函数、默认析构函数也分为有参与无参的函数。由于默认拷贝构造函数需要传入参数,故肯定函数参数。
//1.当我们未提供构造函数和析构函数时,编译器至少为我们提供 默认构造函数(空实现) 拷贝构造函数(拷贝赋值功能) 析构函数(空实现)
//2.如果我们自己提供了 有参构造函数,则编译器不会提供无参构造函数,其他函数仍提供
//3.如果我们自己只提供拷贝构造函数,则其他函数均不提供
class Person
{
public:
Person()
{
cout << "Person的默认(无参)构造函数调用" << endl;
}
Person(int a)
{
m_Age = a;
cout << "Person的有参构造函数调用" << endl;
}
Person(const Person &p)
{
m_Age = p.m_Age;
cout << "Person的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
int m_Age;
};
void test01()
{
Person p1;//当默认构造函数失效时,此方法失效
p1.m_Age = 10;
Person p2(p1);//当拷贝构造函数失效时,此方法失效
cout << "p2.m_Age的值为:"<<p2.m_Age << endl;
}
4.3.6 深拷贝和浅拷贝
4.3.6.1 浅拷贝
同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝。
一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
4.3.6.2 深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
class Person{
public:
Person(char* name,int age){
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName,name);
mAge = age;
}
//增加拷贝构造函数
Person(const Person& person){
pName = (char*)malloc(strlen(person.pName) + 1);
strcpy(pName, person.pName);
mAge = person.mAge;
}
~Person(){
if (pName != NULL){
free(pName);
}
}
private:
char* pName;
int mAge;
};
void test(){
Person p1("Edward",30);
//用对象p1初始化对象p2,调用C++提供的默认拷贝构造函数
Person p2 = p1;
}
4.3.7 多个对象构造和析构
4.3.7.1 初始化列表
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
class Person
{
private:
int mA;
int mB;
int mC;
public:
#if 0
//法一:传统方式初始化
Person(int a, int b, int c)
{
mA = a;
mB = b;
mC = c;
}
#endif
//法二:初始化列表方式初始化
Person(int a, int b, int c): mA(a), mB(b), mC(c) {}
void PrintPerson()
{
cout << "mA:" << mA << endl;
cout << "mB:" << mB << endl;
cout << "mC:" << mC << endl;
}
//法三:使用无参构造函数
Person() : m_A(10), m_B(20), m_C(30){}
};
int main()
{
Person p1(4,5,6); // 必须按照初始化成员列表输入才能定义
p1.PrintPerson();
return 0;
}
**注意:**初始化成员列表(参数列表)只能在构造函数使用。
4.3.7.2 类对象作为成员
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,C++编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?
那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:对于子类调用构造函数,C++为此提供了专门的语法,即构造函数初始化列表。
当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。
析构函数和构造函数调用顺序相反,先构造,后析构。
class phone
{
public:
phone(string phonename)
{
cout << "phone:有参构造函数" << endl;
phone_name = phonename;
}
string phone_name;
~phone()
{
cout << "phone:析构函数" << endl;
}
};
class game
{public:
game(string gamename)
{
cout << "game:有参构造函数" << endl;
game_name = gamename;
}
~game()
{
cout << "game:析构函数" << endl;
}
string game_name;
};
class Person
{
public: //这里由于涉及到别的类,所以m_phone,m_game都必须要有参构造函数
Person(string name, string phonename, string gamename):m_name(name), m_phone(phonename),m_game(gamename)
{
cout << "Person:有参构造函数" << endl;
}
~Person()
{
cout << "Person:析构函数" << endl;
}
void playgame()
{
cout << m_name << "拿着" << m_phone.phone_name << "在玩" << m_game.game_name << "游戏" << endl;
}
string m_name;
phone m_phone;//涉及到手机类
game m_game; //涉及到游戏类
};
int main()
{
Person p1 ("张三", "苹果13", "求生之路2"); //当其他类对象 作为本类成员,先构造其他类对象, 再构造自身,析构的顺序和构造的顺序相反,先调用phone的有参构造函数,在调用game的有参构造函数,最后调用Person的有参构造函数
p1.playgame();
system("pause");
return 0;
}
4.3.8 explicit关键字
C++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。
[explicit注意]:
- explicit用于修饰构造函数,防止隐式转化。
- 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
class MyString
{
public:
MyString(char * str) // 接收字符串
{
}
//explicit用途:防止利用隐式类型转换方式来构造对象
explicit MyString(int len) // 接收字符串长度
{
}
};
void test01()
{ //1.括号方法构造--无歧义
MyString str1(30);
MyString str2("abcd");
//2.隐式方法构造
//MyString str3 = 10;本质等价于MyString str3(10);
//如果写成MyString str3 = 10,会让人误以为str3 = "10"
//explicit防止隐式方法构造,因为隐式方法(也就是上面等号赋值形式)构造容易引起歧义
//故加上explicit之只能写成MyString str3(10),写成MyString str3 = 10报错
}
4.3.9 动态对象创建
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然C早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
然而这些函数在C++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
4.3.9.1 对象创建
当创建一个C++对象时会发生两件事:
-
为对象分配内存
-
调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。C++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。
4.3.9.2 C动态分配内存方法
为了在运行时动态分配内存,C在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用C的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person
{
public:
Person()
{
mAge = 20;
pName = (char *)malloc(strlen("john") + 1);
strcpy(pName, "john");
}
void Init()
{
mAge = 20;
pName = (char*)malloc(strlen("john")+1);
strcpy(pName, "john");
}
void Clean()
{
if(pName != NULL)
{
free(pName);
}
}
public:
int mAge;
char* pName;
};
int main()
{ //分配内存
Person* person = (Person*)malloc(sizeof(Person));
if(person == NULL)
{
return 0;
}
//调用初始化函数
person->Init();
//清理对象
person->Clean();
//释放person对象
free(person);
return 0;
}
问题:
- 程序员必须确定对象的长度。2) malloc返回一个void指针,C++不允许将void赋值给其他任何指针,必须强转。3) malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。4) 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。 C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,C++中我们推荐使用运算符new 和 delete.
4.3.9.3 new operator
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person));
if(person == NULL){
return 0;
}
person->Init(); 构造函数
New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
4.3.9.4 delete operator
new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。
如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Person{
public:
Person(){
cout << "无参构造函数!" << endl;
pName = (char*)malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char* name, int age){
cout << "有参构造函数!" << endl;
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName, name);
mAge = age;
}
void ShowPerson(){
cout << "Name:" << pName << " Age:" << mAge << endl;
}
~Person(){
cout << "析构函数!" << endl;
if (pName != NULL){
delete pName;
pName = NULL;
}
}
public:
char* pName;
int mAge;
};
void test(){
Person* person1 = new Person;
Person* person2 = new Person("John",33);
person1->ShowPerson();
person2->ShowPerson();
delete person1;
delete person2;
}
4.3.9.5 用于数组的new和delete
使用new和delete在堆上创建数组非常容易。
//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100];
//创建整型数组并初始化
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
class Person
{public:
Person(){
pName = (char *) malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char *name, int age){
pName = (char *)malloc(sizeof(name));
strcpy(pName, name);
mAge = age;
}
~Person()
{
if(pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test()
{
//栈聚合初始化
Person person[] = {Person("john", 20), Person("Smith", 22)};
cout << person[1].pName << endl;
//创建堆上对象数组必须体统构造函数
Person* workers = new Person[20];
}
4.3.9.6 delete void*可能会出错
如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数。以下代码未调用析构函数,导致可用内存减少。
class Person{
public:
Person(char* name, int age){
pName = (char*)malloc(sizeof(name));
strcpy(pName,name);
mAge = age;
}
~Person(){
if (pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test(){
void* person = new Person("john",20); // 不调用析构函数
delete person;//故person释放了,person.pName无法释放
}
问题:malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?
答案如下:
-
当申请的空间是内置类型时,delete和free可以混用 当申请的空间是自定义类型时, 1)若没有析构函数,delete和malloc可以混用,有[]和没有[]都相同 2)若申请的空间有析构函数时,malloc申请的空间可以用delete和free释放,但是用delete释放时不能加[] 3)若申请的空间有析构函数时,new申请的空间不能用free释放,可以用delete释放,但是释放时必须加上[]
但是为什么是这样的答案呢?进一步了解区别。
1、new 与 malloc的区别
malloc,是按字节来分配内存的(意思是分配的大小,由自己指定),返回类型是void* ,因此在使用的时候,需要强制转换类型;
new,是先开辟内存(由编译器根据你的对象类型,来决定开辟内存的大小),然后对内存进行初始化(执行构造函数),返回正确指向数据的指针。
返回类型
malloc,返回的是void* 的指针,如果申请内存失败,返回nullptr指针;
new,返回的是申请对象的指针,如果申请内存失败,抛出 bad_alloc类型的异常;因此两者判断是否开辟成功的所做也不一样。
int main()
{
int *p = (int*)malloc(sizeof(int));
if(p == nullptr) {
return 0;
}
.....
free(p);//为了避免野指针的存在,使用完之后给p赋值为nullptr;
int* p = new(int(10))
//if (p == nullptr) 这样判断是错误的
try {
int* p = new(int(10));
delete p;//为了避免野指针的存在,使用完之后给p赋值为nullptr;
}catch (bad_alloc){
}
return 0;
}
2、delete 与 free的区别
执行过程
delete:先调用析构函数,然后释放内存; free 只释放内存。
根据delete的工作步骤,如果是针对于内置数据类型,例如 int,float等,new并不需要执行构造函数;delete并不需要执行析构函数,因此和malloc,free此时并没有什么区别。
图片作者:JACKSONMHLN
4.3.9.7 使用new和delete采用相同形式
Person* person = new Person[10];
delete person;
以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。
我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:一、析构函数;二、释放内存。
那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:
本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。
当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[ ],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论:
如果在new表达式中使用[ ],必须在相应的delete表达式中也使用[ ]。如果在new表达式中不使用[ ], 一定不要在相应的delete表达式中使用[ ]。
4.3.10 静态成员
在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为静态的,称为静态成员。
不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
4.3.10.1 静态成员变量
在一个类中,若将一个成员变量声明为static,这种成员称为静态成员变量。与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享。
静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义。
- 静态数据成员可以通过类名或者对象名来引用。
class Person
{
public:
//静态成员变量,编译阶段就分配了内存
//类内声明\类外初始化
//静态成员变量 所有对象都共享同一份数据
static int age;
private:
static int Id;
};
int Person::age=0; //这里指的是类内的变量,不是全局变量,类外初始化
//一般不用构造函数初始化,因为通过类名访问时,无法触发构造函数的初始化
void test01()
{ //1.通过对象访问
Person a,b;
a.age = 3;
cout << "a.age=" << a.age << endl;
b.age = 4;
cout << "a.age=" <<a.age << endl;
cout << "b.age=" << b.age << endl;
//2.通过类名访问
cout << "Person::age=" << Person::age << endl;
//3.私有静态变量无法访问--静态变量也有访问权限
//cout << Person::Id << endl;
}
4.3.10.2 静态成员函数
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
- 静态成员函数只能访问静态变量,不能访问普通成员变量
- 静态成员函数的使用和静态成员变量一样
- 静态成员函数也有访问权
- 普通成员函数可访问静态成员变量、也可以访问非静态成员变量
class Person
{
public:
//普通成员函数可以访问static 和 non-static 成员属性
void changeParam1(int param)
{
mParam = param;
sNum = param;
}
//静态成员函数只能访问static成员属性
static void changeParam2(int param)
{
//mParam = param; // 无法访问
sNum = param;
}
private:
static void changeParam3(int param)
{
//mParam = param; //无法访问
sNum = param;
}
public:
int mParam;
static int sNum;
};
//静态成员属性类外初始化
int Person::sNum = 0;
int main()
{ //1.类名直接调用
Person::changeParam2(100);
//2.通过对象访问
Person p;
p.changeParam2(200);
//3.静态成员函数也有访问权限
//Person::changeParam3(100); // 类外无法访问私有静态成员函数
Person p1;
p1.changeParam1(200);
return 0;
}
4.3.10.3 const静态成员属性
如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 修饰。定义静态const数据成员时,最好在类内部初始化。
class Person
{
public:
//static const int mShare = 10;
const static int mShare = 10; // 只读,不可修改
};
int main()
{ cout << Person::mShare << endl;
// Person::mShare = 20; 无法修改
return 0;
}
4.3.10.4 静态成员实现单例模式
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。**如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
用单例模式,模拟公司员工使用打印机场景,打印机可以打印员工要输出的内容,并且可以累积打印机使用次数。
class Printer{
public:
static Printer* getInstance() {return pPrinter;}
void PrintText(string text){
cout << "打印内容:" << text << endl;
cout << "已打印次数:" << mTimes << endl;
cout << "--------------"<< endl;
mTimes++;
}
private:
Printer(){ mTimes = 0;}
Printer(const Printer&) {}
private:
static Printer* pPrinter;
int mTimes;
};
Printer* Printer::pPrinter = new Printer;
void test(){
Printer* printer = Printer::getInstance();
printer->PrintText("离职报告!");
printer->PrintText("入职合同!");
printer->PrintText("提交代码!");
}
int main()
{
test();
return 0;
}
明为static,这种成员称为静态成员变量。与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享。
静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义。
- 静态数据成员可以通过类名或者对象名来引用。
class Person
{
public:
//静态成员变量,编译阶段就分配了内存
//类内声明\类外初始化
//静态成员变量 所有对象都共享同一份数据
static int age;
private:
static int Id;
};
int Person::age=0; //这里指的是类内的变量,不是全局变量,类外初始化
//一般不用构造函数初始化,因为通过类名访问时,无法触发构造函数的初始化
void test01()
{ //1.通过对象访问
Person a,b;
a.age = 3;
cout << "a.age=" << a.age << endl;
b.age = 4;
cout << "a.age=" <<a.age << endl;
cout << "b.age=" << b.age << endl;
//2.通过类名访问
cout << "Person::age=" << Person::age << endl;
//3.私有静态变量无法访问--静态变量也有访问权限
//cout << Person::Id << endl;
}
4.3.10.2 静态成员函数
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
- 静态成员函数只能访问静态变量,不能访问普通成员变量
- 静态成员函数的使用和静态成员变量一样
- 静态成员函数也有访问权
- 普通成员函数可访问静态成员变量、也可以访问非静态成员变量
class Person
{
public:
//普通成员函数可以访问static 和 non-static 成员属性
void changeParam1(int param)
{
mParam = param;
sNum = param;
}
//静态成员函数只能访问static成员属性
static void changeParam2(int param)
{
//mParam = param; // 无法访问
sNum = param;
}
private:
static void changeParam3(int param)
{
//mParam = param; //无法访问
sNum = param;
}
public:
int mParam;
static int sNum;
};
//静态成员属性类外初始化
int Person::sNum = 0;
int main()
{ //1.类名直接调用
Person::changeParam2(100);
//2.通过对象访问
Person p;
p.changeParam2(200);
//3.静态成员函数也有访问权限
//Person::changeParam3(100); // 类外无法访问私有静态成员函数
Person p1;
p1.changeParam1(200);
return 0;
}
4.3.10.3 const静态成员属性
如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 修饰。定义静态const数据成员时,最好在类内部初始化。
class Person
{
public:
//static const int mShare = 10;
const static int mShare = 10; // 只读,不可修改
};
int main()
{ cout << Person::mShare << endl;
// Person::mShare = 20; 无法修改
return 0;
}
4.3.10.4 静态成员实现单例模式
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。**如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
用单例模式,模拟公司员工使用打印机场景,打印机可以打印员工要输出的内容,并且可以累积打印机使用次数。
class Printer{
public:
static Printer* getInstance() {return pPrinter;}
void PrintText(string text){
cout << "打印内容:" << text << endl;
cout << "已打印次数:" << mTimes << endl;
cout << "--------------"<< endl;
mTimes++;
}
private:
Printer(){ mTimes = 0;}
Printer(const Printer&) {}
private:
static Printer* pPrinter;
int mTimes;
};
Printer* Printer::pPrinter = new Printer;
void test(){
Printer* printer = Printer::getInstance();
printer->PrintText("离职报告!");
printer->PrintText("入职合同!");
printer->PrintText("提交代码!");
}
int main()
{
test();
return 0;
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)