C++面向过程编程

前言

C语言是面向过程的编程语言,C++是面向对象的编程语言,这是两种不同的编程语言。如果你刚开始学数据结构,可以先暂时认为C语言是C++的子集,C++是C语言的超集,C++进一步扩充和完善了C语言,其中大部分是对于面向对象编程的拓展。C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。

从“Hello world!”讲起

传承学习编程语言的优良传统,我们来写一段“Hello world!”:

#include <iostream>
using namespace std;
int main()
{
   cout << "Hello World";
   return 0;
}

类(class)是用户自定义的数据类型,是一种构造类型,与C语言结构体类似,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数,通过类定义出来的变量也有特定的称呼,叫做“对象”。类一般分为两部分,分别写在不同的文件当中,其一是头文件,用来声明这种类所提供的功能,另一个文件包含了完成这些操作的代码。想要使用类,就必须现在程序中包含头文件。

标准“输入/输出库”

在 C++ 标准的“输入/输出库”名为“ iostream ”,iostream 这个单词是由3个部分组成的,即 i-o-stream ,意为输入输出流。在 iostream 类库中包含许多用于输入输出的类,包含了支持对终端和文件的输入和输出的类。我们必须先包含 中的头文件,才能使用输入输出流进行操作。

cout 和 cin

C++ 的输入输出操作

cout(读作see out) 和 cin(读作see in) 不是运算符,也不是关键字,它们都是C++的内置对象(提前创建好的对象)。cout 和 cin 就分别是 ostream 和 istream 类的对象,是由标准库的开发者提前创建好的,可以直接拿来使用。
C++的输出和输入是用“流”(stream)的方式实现的,cout 是标准输出流对象的,即 ostream 对象,cin 是标准输入流对象,即 istream 对象,关流对象 cin、cout 和流运算符的定义等存放在输入输出流库中,因此如果在程序中使用cin、cout和流运算符,就必须把头文件 stream 包含到本文件中。

“>>” 和 “<<”运算符

符号 功能
>> output运算符,也可以称之为插入器,可以将数据定向到流中,用于向流输入数据
<< input运算符,也可以称之为析取器,可以将输入内容定向到具有适当类型的对象上,用于从流中输入数据

例如:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string name;
	cin >> name;
	cout << "Hello, " 
             << name 
             << "!" 
             << endl; 
	return 0;
}

运行效果为:

在这段代码中,我们先定义了一个 “string” 类型的对象,用于存储输入的内容,然后利用 cin 流和“>>”运算符,把输入的内容存储到“name”对象中,我们将我们希望输出的内容依次通过“<<”输出到 cout 流中。我们可以看到,我们不需要每输入一些内容就写一个“cout << ”,可以将一系列输出语句连接成一条输出语句,以 endl作为结束。

endl

endl是C++标准库中的操控器,英语意思是end of line,即一行输出结束,常与cout搭配使用,意思是输出结束。功能有:

  1. 将换行符写入输出流,并将与设备关联的缓冲区的内容刷到设备中,保证目前为止程序所暂存的所有输出都真正写入输出流;
  2. 清空输出缓冲区。

对比 scanf() 和 printf()

C++ 关键字 cout 和 cin 在时间效率上,其实并不如 scanf() 和 printf()。这个问题深究起来也不难理解,因为这 2 个关键字是对输入和输出的强化,可以适应包括 STL 库容器的输入输出,也自带清理缓冲区等性能。由于这种操作的功能更强大,因此就需要更多的时间来进行附加功能的实现,以及健壮性的处理,因此会耗费更多的时间。而 C 语言的 scanf() 和 printf() 函数来进行输入和输出,功能上就比较弱一些,所以执行的速度就比较快。即使有效率上的问题,但是还是很推荐使用 cout 和 cin 进行功能更强的输入和输出。

“using namespace std”是什么?

std 是标准库所驻的命名空间的名称,标准库提供的任何内容都被封装在命名空间 std 中。命名空间是一种将库名称封装起来的方法,可以避免和应用程序发生命名冲突的问题。因此,若要在程序中使用 string class 以及 cin、cout 对象,除了要包含头文件,还要用 using namespace 令命名空间 std 内定义的所有标识符都有效,就好像它们被声明为全局变量一样。
如果没有写上这句代码,由于没有声明命名空间 std 中的对象有效,因此还使用 cout 来输出就会报如下错误,也就是变量没有声明。

[Error] 'cout' was not declared in this scope


如果想要在不写 using namespace std 的情况下使用命名空间 std 中的 cout 对象,就需要改为如下写法。

#include <iostream>

int main()
{
	std::cout << "Hello World";
	return 0;
}

初始化

C++为我们提供了另一种初始化的方式,即“构造函数语法”,形如:

int num(0);

不过我们提到了初始化,很自然我们会这么做:

int num = 1;

这么做当然没有问题了,我们学C语言的时候都是这么干的。但是为什么C++会提供另外一种初始化的方式呢?
举个例子,C++的标准库有一种类叫做复数类,一个复数类对象由2部分组成,分别是实部和虚部,这也就说明了我们初始化复数类对象时,需要同时对实部和虚部进行初始化,这时候原来的“用 assignment 运算符(=)初始化就不好用了。
为了解决“多值初始化的问题”,C++提供了构造函数语法:

#include <complex>
complex<double> purei(0,1);

引用

什么是引用

下面的写法定义了一个引用,并将其初始化为引用某个变量。

类型名 & 引用名 = 变量名

引用变量是一个别名,某个变量的引用等价于这个变量。把引用初始化为某个变量,就可以使用该引用名来指向变量,原来的变量名仍然有效,仍可以用原来的变量名来指向变量。引用有以下注意事项:

  1. 不能是空引用,定义引用时必须初始化为引用某个变量;
  2. 引用是从一而终的,初始化饮用之后就一直引用初始化的变量,不能引用其他变量;
  3. 引用只能引用变量,不能引用常量和表达式。

为了更直观地理解引用,我们来写段代码:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	int num1(0);
	int & num2 = num1;
	
	cout << num2 << endl; 
	
	num1 = 1;
	cout << num1 << endl; 
	cout << num2 << endl;
	
	num2 = 2;
	cout << num1 << endl; 
	cout << num2 << endl;
	return 0;
}

运行结果为:

我们首先定义了一个 int 类型的对象 num1,然后定义了一个引用 num2。在我们定义引用之后,num1 和 num2 这两个就是一回事了,我们对 num1 操作等同于对 num2 操作,因此我们每次修改之后,输出 num1 和输出 num2 的数据是相同的。

常引用

定义引用时,在定义的语句前加 const 关键字,此时将定义一个常引用。常引用的作用是不能通过引用去修改其引用的内容,写法为:

const 类型名 & 引用名 = 变量名
  • 常引用不能修改引用的内容,但是被引用的内容本身可以修改。

常引用和非常引用

常引用和非常引用是不同的类型,引用和变量可以用来初始化常引用,但是常引用不能用来初始化引用。

实例


我们先还原一下我们该开始学编程时会犯的错误:

void swap(int a,int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

我们都知道,这么写是错的,因为这涉及到实参和形参问题。传入的变量 a、b 是对原来的数据的一个拷贝,是局部变量,一旦函数执行完毕, a、b 两个变量就会消失。那后来我们是怎么解决这个问题的呢?我们可以传指针进入函数,这样就可以通过指针间接操作变量。
但是现在我们可以用引用来解决这个问题:

#include <iostream>
#include <string>
using namespace std;

void swap(int & a,int & b);

int main()
{
	int num1(0);
	int num2(1);
	
	swap(num1, num2);
	cout << "num1 = " << num1 << endl; 
	cout << "num2 = " << num2 << endl;
	
	return 0;
}

void swap(int & a,int & b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

输出结果为:

我们将 a、b 的类型改为了引用,当我们向函数传值的时候,a 将引用传入的第一个参数,b 将引用传入的第二个参数。根据引用的定义,a、b 就分别和传入的两个变量是一回事了,这个时候我们操作 a、b,就等同于操作传入的参数本身,就不需要想以前那样传入地址了。

引用和指针

引用和指针有一定的相似之处,它们主要有三处不同:

  1. 引用不能是空引用,引用必须连接到一块合法的内存。指针可以是空指针。
  2. 引用被初始化为一个对象,之后就不能被引用其他对象。指针可以在任何时候指向另一个对象。
  3. 引用必须在创建时被初始化。指针可以在任何时间被初始化。

动态内存分配

分配

我们在C++中使用 new 运算符实现动态内存分配。
当我们只分配一个变量时,语法如下:

ptr = new Type

当我们分配一个数组时,语法如下:

ptr = new Type[N]
参数 说明
Type 任意类型名,可以是 class 类型
ptr Type* 类型的指针名
N 分配的数组元素个数,可以是整型表达式

使用 new 运算符之后,C++ 将分配出大小为 sizeof(Type) × N(分配变量时N为1) 的空间,并且将内存空间的起始地址赋值给 ptr 。new 运算符自动计算要分配类型的大小,不使用 sizeof 运算符,较为便捷且可以避免错误。new 运算符能自动地返回正确的指针类型,因此不用进行强制指针类型转换。如果内存分配失败,和 malloc函数 一样,new 表达式返回 NULL,因此可以通过检查返回值的方式得知内存分配成功与否。

释放

有申请空间就一定要释放,用 new 运算符分配的内存空间需要用 delete 运算符释放。
申请变量时,释放语法为:

delete ptr;

申请指针时,释放语法为:

delete []ptr;
参数 说明
ptr 需要释放的空间的指针名,必须是动态内存分配出的空间
  • 我们无需检验 ptr 是否为空指针

内存泄漏

程序中动态分配了内存,却没有回收,这就造成这部分存储空间不能再被利用,称为内存泄漏(memory leak),如果程序需要长期运行,内存泄漏问题会耗尽所有内存,从而使程序崩溃。所以养成好的习惯,在所分配的内存不再使用后,及时回收内存。

new 和 malloc的区别

  1. new分配的空间不需要强制类型转换;
  2. new分配的空间不需要判空;
  3. new分配的空间时可以初始化。

缺省值

给函数参数赋默认值

在C++中,定义函数时,我们可以让最右边的连续若干个参数具有默认值,这个默认值叫做缺省值。调用参数的时候,若相应位置不写参数,那么该参数的值就会被赋值为缺省值。

实例


代码实现:

int add(int a, int b = 20, int c = 30)
{
    return a + b + c;
}

提供默认值的规则

  1. 默认值的解析操作从最右边开始。如果我们为某个参数提供了默认值,则该参数右侧的所有参数都必须具有默认值;
  2. 默认值只能指定一次,即函数声明和函数定义只能有一个地方指定默认值,不能两个地方都指定。
  • 为了提高可见性,建议默认值在函数声明的时候指定。

内联函数

为什么会有“内联函数”

当我们用函数封装代码时,函数的调用是有开销的,除了时间上的开销,频繁调用的函数也可能大量消耗栈空间。如果我们写了个功能强大的函数,这个函数有几百行,那么函数调用的开销相对会显得比较小,可以忽略不计,就好比分析卫星运行的轨道半径时不需要考虑卫星的长度参数一样。但是如果是个只有几条语句,运行速度很快的函数,例如:

这个函数的作用只是打印一条分割线,而且在程序中需要被多次调用。相比之下,这种函数被调用的开销就成为了需要考虑的因素了。

inline 关键字

为了减少函数调用的开销,C++引入了内联函数的机制。将函数声明为 inline,表示建议编译器在调用这个函数的时候,将函数的内容展开,此时编译器将会把函数的调用操作改为以一份代码副本来替代。
例如我们要输出100个“生日快乐!”:

#include <iostream>
using namespace std;

void put_a_message();

int main()
{
   for(int i = 0; i < 100; i++)
        put_a_message();
   return 0;
}

inline void put_a_message()
{
    cout << "生日快乐!" << endl;
}

当我们把 put_a_message 函数定义为内联函数时,在内部就可能是以这种方式实现功能:

#include <iostream>
using namespace std;
int main()
{
   for(int i = 0; i < 100; i++)
        cout << "生日快乐!" << endl;
   return 0;
}

使用 inline 的注意事项

  1. 使用限制:inline 只适合代码简单的函数使用,不能包含复杂的结构控制语句例如 while、switch,并且不能内联函数本身不能是直接递归函数。
  2. inline是一个建议:将函数定义为 inline 函数仅仅是对编译器提出的一个要求,编译器不一定执行这项要求,所以最后能否真正内联,看编译器的意思。如果编译器认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联。
  3. 适合定义为 inline 的函数:一般来说,最适合定义为内联函数的函数的特点为:体积小、需要被多次调用、执行的操作简单。
  4. inline 函数定义:由于内联函数要在被调用的时候展开,所以编译器必须随处可见内联函数的定义,因此 inline 函数的定义常放于头文件。
  5. inline 是"用于实现的关键字":关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

函数重载

为什么会有“函数重载”

例如我们要写3个函数,分别要求出两个整数,三个整数,两个双精度数的最大值。那么我们把这3和函数都命名为“max”是一件合情合理的事情,但是学习C语言时我们知道不同的函数时不能取相同的函数名的,这就需要我们为这3个函数分别去不同的函数名,甚至会取得又臭又长。

函数重载

我们实现一下前文的代码:

#include <iostream>
using namespace std;
int myMax(int a, int b) 
{
    return (a > b ? a : b);
}

int myMax(int a, int b, int c)
{
    b > a ? a = b : a = a;
    return (a > c ? a : c);
}

double myMax(double a, double b)
{
    return (a > b ? a : b);
}

int main()
{
    cout << myMax(3,4) << endl;
    cout << myMax(3,4,5) << endl;
    cout << myMax(4.3,3.4) << endl;
}

C++允许我们写出好几个具有相同函数名的函数。一个或多个函数名相同,但是函数个数或参数类型不同的函数,叫做函数重载。
函数重载使得函数命名变得简单,在调用函数的时候,编译器会将调用函数时提供的实际参数和每个重载函数的参数对比,判断应该调用哪个函数。

  • 编译器无法根据函数的返回类型来区分两个函数名相同的函数。

函数指针

指向函数的指针

每一个函数都占用一段内存单元,它们有一个起始地址,指向函数入口地址的指针称为函数指针。语法为:

Type (*ptr)(list);
参数 说明
Type 函数的返回值的类型
ptr 指针变量名
list 调用函数时传递的参数表

在函数指针变量赋值时,只需给出函数名,不必给出参数。函数指针变量指向的函数不是固定的,这表示定义了一个这样类型的变量,用来存放函数的入口地址,函数指针指向的函数由我们在程序中把哪一个函数的地址赋给它而决定。

实例


代码实现:

参考资料

《新标准C++程序设计》——郭炜 编著,高等教育出版社
《Essential C++》——[美]Stanley B.Lippman 著,侯捷 译,电子工业出版社
《C++ Primer》——[美] Stanley B. Lippman Barbara E. Moo Josée LaJoie,译 李师贤 等
C++里面的iostream是什么
C++构造函数初始化类对象
C++ new和malloc的区别
C++函数指针详解
菜鸟教程
C语言中文网

posted @ 2020-02-11 18:24  乌漆WhiteMoon  阅读(2263)  评论(2编辑  收藏  举报