C++语言对C语言的扩充

1.1 C++的特点

C++语言既保留了C语言的有效性、灵活性、便于移植等全部精华和特点,又添加了面向对象编程泛型编程的支持,具有强大的编程功能,可方便地构造出模拟现实问题的实体和操作;编写出的程序具有结构清晰、易于扩充等优良特性,适合于各种应用软件、系统软件的程序设计。用C++编写的程序可读性好,生成的代码质量高,运行效率仅比汇编语言慢10%~20%。

1.2 C++语言的文件扩展名

为了使编译器能够区别是C语言还是C++语言,C++语言体系规定用“.cpp”(意即C Plus-Plus)作为C++语言源文件的扩展名以区别于C语言用的“.C”文件扩展名。虽然仅差两个字母,但编译时的处理却相差甚远。“cpp”的文件扩展名与操作系统无关。与C++语言源文件相关的头文件扩展名一般仍用“.h”,但有些操作系统也有规定使用“.hpp”充当头文件扩展名的。

1.3 注释符

1.4 命名空间

命名空间域是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域),可以用花括号把文件的一部分括起来,并以关键字namespace开头给它起一个名字:

namespace ns1
{
     float a, b, c;
     fun1(){    }
}

花括号括起来的部分称声明块。声明块中可以包括:类、变量(带有初始化)、函数(带有定义)等。在域外使用域内的成员时,需加上命名空间名作为前缀,后面加上域操作符“::” 。这里添加了命名空间名称的成员名被称为限定修饰名。如:ns1::ans1::fun1()等等。
最外层的命名空间域称为全局命名空间域(Global Namespace Scope),即文件域。
命名空间域可分层嵌套,同样有分层屏蔽作用。例如:

namespace n1 {
    namespace n2 {	    // 命名空间嵌套
        class matrix{   }	// 命名空间类成员matrix
    }
}

访问matrix类,可写成:n1::n2::matrix

使用using声明可只写一次限定修饰名。using声明以关键字using开头,后面是被限定修饰的(qualified)名字空间成员名,例如:
using n1::n2::matrix; // 命名空间类成员matrix的using声明
以后在程序中使用matrix时,就可以直接使用成员名,而不必使用限定修饰名。
使用using指示符可以一次性地使名字空间中所有成员都可以直接被使用,比using声明方便。using指示符以关键字using开头,后面是关键字namespace,然后是名字空间名。

标准C++库中的所有组件都是在一个被称为std的名字空间中声明和定义的。在采用标准C++的平台上使用标准C++库中的组件,只要写一个using指示符:using namespace std;
就可以直接使用标准C++库中的所有成员。
注意:如果使用了名空间std,则在使用#include编译预处理命令包含头文件时,必须去掉头文件的扩展名.h,否则会出错。

1.5 C++语言的输入输出

C++语言另外定义了一套保留字与运算符来替代C语言中对标准输入、输出函数的引用。C++语言的保留字为:

cout << "输出内容" << …;       // cout为标准输出流对象(默认输出到显示器)
cin >> "输入内容" >> …;        // cin为标准输入流对象(默认从键盘输入)
#include <iostream.h>

举个栗子

#include<iostream>            // 使用名空间std,则必须去掉.h扩展名
using namespace std; 
void main() {  
    char  name[10];
    int  age;
    cout << "请输入您的名字:";
    cin >> name;
    cout << "请输入您的年龄:";
    cin >> age;
    cout << "姓名为:" << name << endl;
    cout << "年龄为:" << age << endl;
}

1.6 变量的定义

在C语言中,局部变量说明必须置于可执行代码段之前,不允许局部变量声明和可执行代码混合在一起。但C++在变量的定义上作了两种较大的改变

1.7 强制类型转换

1.8 动态内存的分配与释放

1.new运算符

指针变量 = new 数据类型;

2.delete运算符

运算符delete用于释放new分配的内存空间,它的使用形式为:delete 指针变量;
其中的指针变量保存着new动态分配的内存的首地址。
举个栗子

#include <iostream>
using namespace std;
void main(){
    int *p;
    p = new int;    // 分配内存空间
    *p = 5;
    cout << *p;
    delete p;       // 释放内存空间
}

在用new分配内存的同时进行初始化。使用形式为:指针变量 = new 数据类型(初始值);
例如上例中的:p = new int; *p = 5;
也可写成:p=new int(5);

3. 注意:

(1) 用new获取的内存空间,必须用delete进行释放;
(2) 对一个指针只能调用一次delete;
(3) 用delete运算符作用的对象必须是用new分配的内存空间的首地址。

4.用new建立数组类型的变量

指针变量 = new 数据类型[数组大小];
此时指针变量指向第一个数组元素的地址。使用new分配数组时,不能提供初始值。使用new建立的数组变量也由delete释放。其形式为:delete 指针变量;delete [ ]指针变量;
同样,也可以用new来为多维数组分配空间,但是除第一维可以为变量外,其它维数都必须是常量。

注意在使用delete时,不用考虑数组的维数。
有时,并不能保证一定可以从堆内存中获得所需空间,当不能成功地分配到所需要的内存时,new返回0,即空指针。因此我们可以通过判断new的返回值是否为0,来得知系统中是否有足够的空闲内存来供程序使用。例如:

int *p = new int[100];
if (p == 0) {
    cout<< "已终止, 不能分配更多内存. "<<endl;
    exit(1);
}

其中exit函数的作用是终止程序运行。

举个栗子:从堆内存中获取一个整型数组,赋值后并打印出来。

#include <iostream>
using namespace std;
int main() { 
    int n; // 定义数组元素的个数
    int *p;            
    cout << "请输入数组长度 : ";
    cin >> n;
	
    if ((p = new int[n]) == 0) {    // 分配内存空间
        cout<< "已终止, 不能分配更多内存. "<<endl;
        exit(1); 
    }         
    for (int i = 0; i < n; i++) {	   
        p[i] = i * 2;
    }
    cout << "输出数组: " << endl;
    for (i = 0; i < n; i++) {
        cout << p[i] << "  ";
    }
    cout << endl;
    delete []p;     // 释放内存空间
    return 0;
}

1.9 作用域运算符::

通常情况下,如果全局变量与局部变量同名,那么局部变量在其作用域内具有较高的优先权。C语言规定只能在变量的作用域内使用该变量,不能使用其他作用域中的变量,可采用C++中提供的作用域运算符::,它能指定所需要的作用域。
注意:不能用::访问函数中的局部变量。在C++语言中作用域运算符::还用来限定类的成员

#include <iostream>   
using namespace std;
float attack = 2000;        // 全局变量
int main(){  
    int attack = 1000;      // 局部变量
    cout << attack << endl;
    cout << ::attack << endl;    // ::a表示全局作用域中的变量a
    return 0;
} 

1.10 引用

引用是C++语言的一个特殊的数据类型描述,用于在程序的不同部分使用两个以上的变量名指向同一地址,使得对其中任一个变量的操作实际上都是对同一地址单元进行的。在这种两个以上变量名的关系上,被声明为引用类型的变量名则是实际变量名的别名。
引用运算符为&,声明引用的一般形式为:
数据类型 &引用变量名 = 变量名; 数据类型 &引用变量名 = 变量名;数据类型 &引用变量名 = 变量名;
对引用进行操作,实际上就是对被引用的变量进行操作。引用不是值,不占存储空间,声明引用时,目标的存储状态不会改变。引用一旦被初始化,就不能再重新赋值。
举个栗子

#include <iostream>
using namespace std;
int main(){  
    int num = 50;	
    int &ref = num;
	ref += 10;
	cout << "num = " << num << endl;
	cout << "ref = " << ref << endl;
        num += 40;
	cout << "num = " << num << endl;
	cout << "ref = " << ref << endl;
	return 0;
}

说明:
(1) 在一行上声明多个引用型变量(函数)名时,要在每个变量(函数)名前都冠以“&”符号。
(2) 引用不是变量,所以引用本身不能被修改,在程序中对引用的存取都是对它所引用的变量的存取。
(3) 一个变量被声明为引用时必须进行初始化,除非这个引用是用作函数的参数或返回值,为引用提供的初始值应为变量(包括对象)。引用一旦被初始化,就不能再重新赋值。如ref = &j;ref = j;是允许的。
(4) 由于引用不是变量,所以,不能说明引用的引用,也不能说明数组元素的类型为引用数组,或指向引用的指针。例如:
int &a[5]; // 错误
int &*p; // 错误
由于指针也是变量,因此可以说明对指针变量的引用。例如:

int *a;  
int *&p = a;   
int b;  
p = &b;    // a指向变量b

(5) 引用与指针不同。指针的内容或值是某一变量的内存单元地址,而引用则与初始化它的变量具有相同的内存单元地址。指针是个变量,可以把它再赋值成其它的地址,然而,建立引用时必须进行初始化并且决不会再指向其它不同的变量。
(6) 要注意区分引用运算符和地址运算符的区别。例如:

int num = 50;
int &ref = num;
int *p = &ref;

(7) 可以用一个引用初始化另一个引用。例如:

int num = 50;
int &ref1 = num;
int &ref2 = ref1;
ref2 = 100;			// num被修改为100

其中ref2也是对num的引用。
(8) 可以把函数的参数说明成引用以建立函数参数的引用传递方式
(9) 有空指针,无空引用
(10) 引用不能用数据类型来初始化。如:int&ref=int; // 错误
(11) 函数调用可以作为左值
引用表达式是一个左值表达式,因此它可以出现在形、实参数的任何一方。若一个函数返回了引用,那么该函数的调用也可以被赋值。一般说,当返回值不是本函数内定义的局部变量时就可以返回一个引用。在通常情况下,引用返回值只用在需对函数的调用重新赋值的场合,也就是对函数的返回值重新赋值的时候。避免将局部作用域中变量的地址返回,就使函数调用表达式作为左值来使用。

栗子:统计学生中A类学生与B类学生各为多少个。A类学生的标准是平均分在80分以上,其余都是B类学生。

#include <iostream>   
using namespace std;
int array[6][4] = {{60, 80, 90, 75}, {75, 85, 65, 77}, {80, 88, 90, 98},
                 {89, 100, 78, 81}, {62, 68, 69, 75}, {85, 85, 77, 91}};
int& level(int grade[], int size, int& tA, int& tB);
int main() {
    int typeA = 0,typeB = 0;
    int student = 6;
    int gradesize = 4;
    for (int i = 0; i < student; i++)   // 对所有的学生数据进行处理
        level(array[i], gradesize, typeA, typeB)++; // 函数调用作为左值
    cout << "number of type A is " << typeA << endl;
    cout << "number of type B is " << typeB << endl;
    return 0;
}
int& level(int grade[], int size, int& tA, int& tB) {
    int sum = 0;
    for(int i = 0; i < size; i++)   // 成绩总分
        sum += grade[i];
    sum /= size;                  // 平均分
    if(sum >= 80) return tA;    // A类学生
    else    return tB;          // B类学生
}
程序的运行结果为:
number of type A is 3
number of type B is 3

栗子:返回的局部作用域内的变量,函数作为左值。

#include <iostream>   
using namespace std;
float& fn2(float r){
    float temp;
    temp = r * r * 3.14;
    return temp;          // 返回了局部变量
}
int main() {
    fn2(5.0) = 12.4;
    /* 返回的是局部作用域内的变量,函数调用作为左值使用。此种情况应尽量避免。*/
    return 0;
}

此程序在编译时会出现如下的警告信息:
warning C4172: returning address of local variable or temporary


1.11 const修饰符

#define PI 3.1415926
const float PI = 3.1415926; 

这个常量是有类型的,它有地址,可以用指针指向这个值,但不能修改它。C++建议用const取代#define定义常量
注意:
(1) 使用const修饰符定义常量时,必须初始化;
(2) 常量一旦被定义,在程序中任何地方都不能再更改。
(3) 如果用const定义的是一个整型常量,int可以省略。
(4) 与#define定义的常量有所不同,const定义的常量可以有自己的数据类型,这样C++编译程序可以进行更加严格的类型检查,具有良好的编译时的检测性。
(5) 函数参数也可以用const说明,用于保证实参在该函数内部不被改动,大多数C++编译器能对具有const参数的函数进行更好的代码优化。例如,通过函数max求出整型数组a[100]中的最大值,函数原型应该是:int max(const int*pa);
这样做的目的是确保原数组的数据不被破坏,即在函数中对数组元素的操作只许读,不许写。


const与指针一起使用的组合情况:
(1) 指向常量的指针
指向常量的指针是指一个指向常量的指针变量。const char* pc= "abcd";
声明指向常量的指针变量pc,它指向一个字符串常量
由于使用了const,不允许改变指针所指的常量,因此以下语句是错误的:
pc[3]='x';
但是由于pc是一个指向常量的普通指针变量,不是常指针,因此可以改变pc的值。例如以下语句是允许的:pc="jkkk";
(2) 常指针
常指针是指指针本身,而不是它指向的对象声明为常量。例如:char*const pc="abcd"; // 常指针
这个语句的含义为:声明一个名为pc的指针变量,该指针是指向字符型数据的常指针,用“abcd”的地址初始化该常指针。创建一个常指针,就是创建不能移动的固定指针,但是它所指的数据可以改变。例如:pc[3]='x'; //合法 pc="dfasdfa"; //不合法
(3) 指向常量的常指针
整个指针本身不能改变,它所指向的值也不能改变。要声明一个指向常量的常指针,二者都要声明为const。例如:const char* const pc="abcd"; // 指向常量的常指针
这个语句的含义为:声明一个名为pc的指针变量,它是一个指向字符型常量的常指针,用“abcd”的地址初始化该指针。以下两个语句都是错误的:
pc[3]='x'; // 错误,不能改变指针所指的值 pc="dfasdfa"; // 错误,不能改变指针本身


1.12 字符串

除了计算外,文本处理也是编程过程中一个非常重要的方面。在C语言中使用字符数组和字符指针实现字符串,但是在C++中提供了一种既方便又好用的string类型。下面通过一个简单的例子说明string类型的使用。

#include <iostream>   
#include <string>      // 使用string类型需包含头文件<string>
using namespace std;
int main(){
	string s,t;
	cout << "请输入一个字符串:" << endl;
	cin >> s;    // 由键盘输入一行文本,并把它赋给sring类型的变量s
	t = "I like programming!";
	cout << "字符串的输出:" << endl << s << endl << t << endl;
	cout << s.append(" OK!") << endl;  // append为string类的成员函数
	return 0;
}


1.13 C++语言中函数的新特性

1.13.1 函数原型(Function Prototype)

  • C++要求为每一个函数建立原型,用以说明函数的名称、参数个数及类型和函数返回值的类型
  • 其主要目的是让C++编译程序进行类型检查,即形参与实参的类型匹配检查,以及返回值是否与原型相符,以维护程序的正确性。所以应养成将声明与定义分别编写的编程习惯。
  • 函数原型与函数的定义要在函数的返回类型,函数名和参数的类型及数量这三条线上保持一致。
  • 当然,在写函数原型时,可以省略形参的名字,因为参数名对编译器没有意义,但如果取名恰当的话,这些名字可以起到提示参数用途的作用。

1.13.2 内联(inline)函数

  • 在执行程序过程中如果要进行函数调用,则系统要将程序当前的一些状态信息存到栈中,之后进行虚实结合,同时转到函数的代码处去执行函数体语句,这些参数保存与传递的过程中需要时间和空间的开销,使得程序执行效率降低,特别是在程序频繁地进行函数调用以及函数代码段比较少时,这个问题会变得更为严重。为了解决这个问题,C++引入了内联函数机制。
  • 使用内联函数是一种用空间换时间的措施,若内联函数较长,且调用太频繁时,程序将加长很多。因此,通常只有较短的函数才定义为内联函数,对于较长的函数最好作为一般函数处理。

一般情况下,我们对内联函数做如下的限制:

#include <iostream>   
using namespace std;
inline double circumference(double radius);
// 内联函数的声明,如果此处省略inline关键字,即使在函数定义时加上//inline关键字,
// 编译程序也不认为那是内联函数,对待该函数如普通函数那样,
// 产生该函数的调用代码并进行连接。
int main() {
    double r=3.0,s;
    s = circumference(r);
    cout << "the circumference is " << s << endl;
    return 0;
} 
// 内联函数的定义,此处也可以省略inline关键字
inline double circumference(double radius) {
    return 2 * 3.1415926 * radius;
}

程序的运行结果为:

the circumference is 18.8496

1.13.3 带缺省参数的函数

如果在函数说明或函数定义中为形参指定一个缺省值,则称此函数为带缺省参数的函数。当函数调用发生后,在形参表中等号后的各“缺省值”将起实参的传递作用。
如果函数有多个缺省参数,则缺省参数必须是从右向左定义,并且在一个缺省参数的右边不能有未指定缺省值的参数

void fun(int a = 3,int b = 6,int c, int d);
void fun(int a = 65, int b = 3, int c, int d = 3);

需要特别注意的是如果在函数原型的声明中设置了函数参数的缺省值,则不可再在函数定义的头部重复设置,否则编译时将出现错误信息。(思考:如果函数原型中无缺省值,定义时有,正确吗?)

1.13.4 函数重载(overload)

C++编译系统允许为两个或两个以上的函数取相同的函数名,但是形参的个数或者形参的类型不应相同,编译系统会根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是所谓的函数重载
函数重载无需特别声明,只要所定义的函数与已经定义的同名函数形参形式不完全相同,C++编译器就认为是函数的重载。

栗子:

#include <iostream>
using namespace std;
int add(int x, int y) {
    int sum;
    sum = x + y;
    return sum;
}
int add(int x, int y, int z) {
    int sum;
    sum = x + y + z;
    return sum;
}
void main( ) {
    int a,b;
    a = add(5, 10);
    b = add(5, 10, 20);
    cout << "a = " << a<< endl;
    cout << "b = " << b<< endl;
}
程序运行结果为:
a = 15
b = 35

在使用重载函数时要注意:
①不可以定义两个具有相同名称、相同参数类型和相同参数个数,只是函数返回值不同的函数。
int func(int x);
float func(int x);
②如果某个函数参数有缺省值,必须保证其参数缺省后调用形式不与其它函数混淆。
int f(int a, float b);
void f(int a, float b, int c=0);
函数调用语句:
f(10, 2.0);
具有二义性,既可以调用第一个函数,也可以调用第二个函数,编译器不能根据参数的形式确定到底调用哪一个。

1.13.5 函数模板(function template)

C++语言中可以使用模板来避免在程序中多次书写相同的代码。所谓模板是一种使用无类型参数来产生一系列函数或类的机制,是C++的一个重要特征。它的实现方法方便了更大规模的软件开发。模板是以一种完全通用的方法来设计函数和类,而不必预先说明将被使用的每个对象的数据类型。通过模板可以产生类或函数的集合,使它们操作不同的数据类型,从而避免为每一种数据类型产生一个单独的类或函数。
模板分为函数模板和类模板,C++提供的函数模板可以定义一个对任何类型变量进行操作的函数,从而大大增强了函数设计的通用性。这是因为普通函数只能传递变量参数,而函数模板提供了传递类型的机制。使用函数模板的方法是先说明函数模板,然后实例化成相应的模板函数进行调用执行。

1.函数模板

函数模板的一般说明形式如下:

template<模板参数表><返回值类型><函数名> (模板函数形参表) {
    // 函数定义体
}

<模板参数表>尖括号中不能为空,参数可以有多个,用逗号分开。模板参数主要是模板类型参数。模板类型参数(template type parameter)代表一种类型,由关键字 class 或 typename后加一个标识符构成,在这里两个关键字的意义相同,它们表示后面的参数名代表一个基本数据类型或用户定义的类型。

如果类型形参多于一个,则每个类型形参都要使用class或typename。<模板函数形参表>中的参数必须是唯一的,而且<函数定义体>中至少出现一次。
如:template<class T>,则“T”可以在程序运行时被任何C++语言支持的数据类型所取代。如有两个以上的模板参数时,使用逗号分隔,如:template<class T1, class T2>。由于模板是专门为函数安排的,所以模板声明语句必须置于与其相关的函数声明或定义语句之前,但附于函数声明语句和定义语句前的模板参数表的替代类型标识符可以不一致。
函数模板定义不是一个实实在在的函数,编译系统不为其产生任何执行代码。该定义只是对函数的描述,表示它每次能单独处理在类型形式参数表中说明的数据类型。

栗子:编写一个对具有n个元素的数组a[]求最小值的程序,将求最小值的函数设计成函数模板。

#include <iostream>
using namespace std;
template<class T> T min(T a[ ], int n) {
    int i;
    T minv = a[0];
    for (i = 1; i < n; i++) {
        if(minv > a[i]) {
            minv = a[i];
        }
    }
    return minv;
}
int main() {
    int a[ ] = {1, 3, 0, 2, 7, 6, 4, 5, 2};
    double b[ ] = {1.2, -3.4, 6.8, 9, 8};
    cout << "a数组的最小值为:" << min(a, 9) << endl;
    cout << "b数组的最小值为:" << min(b, 4) << endl;
    return 0;
}

程序的运行结果为:

a数组的最小值为:0
b数组的最小值为:-3.4

注意:此例中,如果不采用函数模板,则需要用户自义以下两个函数(这两个函数的算法完全相同)才可以使程序正常运行,通过此例我们也可以体会到使用模板的好处。

double min(double a[ ], int n) {
    int i;
    double minv = a[0];
    for (i = 1; i < n; i++) {
        if(minv > a[i]) {
            minv = a[i];
        }
    }
    return minv;
}
int min(int a[ ], int n) {
    int i;
    int minv = a[0];
    for (i = 1; i < n; i++) {
        if(minv > a[i]) {
            minv = a[i];
        }
    }
    return minv;
}

2.模板函数

  • 函数模板只是说明,不能直接执行,需要实例化为模板函数后才能执行。当编译系统发现有一个函数调用: <函数名><实参表>;时,将根据<实参表>中的类型生成一个重载函数,即模板函数。该模板函数的定义体与函数模板的函数定义体相同,而<形参表>的类型则以<实参表>的实际类型为依据。

  • 模板函数有一个特点,虽然模板参数T可以实例化成各种类型,但是采用模板参数T的各参数之间必须保持完全一致的类型。模板类型并不具有隐式的类型转换,例如在int与char之间、float与int之间、float与double之间等的隐式类型转换

  • 函数模板方法克服了C语言用大量不同函数名表示相似功能的坏习惯;克服了宏定义不能进行参数类型检查的弊端;克服了C++函数重载用相同函数名字重写几个函数的烦琐。

3.函数模板与重载函数
当模板函数与重载函数同时出现在一个程序体内时,C++编译器的求解次序是先调用重载函数;如果不匹配,则调用模板函数;如果还不匹配则进行强制类型转换,前面几种方法都不对,则最后报告出错。

栗子:模板函数与重载函数。

#include <iostream>
using namespace std;
#define PI 3.1415926535

template<class T> double Circle_Square(T x) {
  return x * x * PI;
}
double Circle_Square(long x) {
  return x * x * PI;
}
int main() {
    int r1=1;
    double r2=2.0;
    long r3=3;
    cout << "The first circle square is  " << Circle_Square(r1) << endl
	     << "The second circle square is " << Circle_Square(r2) << endl
	     << "The third circle square is  " << Circle_Square(r3) << endl;
    return 0;
}
程序的运行结果为:
The first circle square is  3.14159
The second circle square is 12.5664
The third circle square is  28.2743

说明:本例中Circle_Square(r1)Circle_Square(r2)调用的是模板函数,而Circle_Square(r3)调用的是重载函数double Circle_Square(long x)

posted @ 2021-07-05 21:25  可燃冰丶  阅读(707)  评论(0)    收藏  举报