从C到C++

C++是C语言的扩展和超集,也是现今程序设计语言中最为复杂的一员,要熟练地掌握和运用C++,没有十年半载的时间是不大可能的,但是这篇文章我只是想简单的讨论一下C++相比于C扩展的地方以及不同之处,以便于从C快速的进阶到C++,为之后的C++版数据结构做准备,与C相同的地方将不再赘述,但要注意,这些只是简单的了解,并不能算是真正的学会。会用一门编程语言实现“HelloWorld”并不等于就掌握了这门编程语言。

程序基础

按照古典的传统,先用C++实现一个“HelloWorld”,代码如下:

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

第1行使用预处理指令#include将头文件iostream包含到程序中来;第2行的作用我们后面再说;第3行定义了一个main()函数;第4-7行是函数体的内容,cout是C++标准的输出流,符号“<<”是一个输出运算符,带一个输出流作为它的左操作数,一个表达式作为它的右操作数,后者被发送到前者。类似的,cin是C++标准的输入流,符号“>>”是一个输入运算符,带一个输入流作为它的左操作数,一个变量作为它的右操作数,前者被抽取到后者。另外,endl表示换行。

在输入/输出方面,C++中引入了“”这个概念,流是输入/输出设备的另一个名字,每个I/O设备传送和接收一系列的字节,称之为流。输入操作可以看成是字节从一个设备流入内存,而输出操作可以看成是字节从内存流出到一个设备。使用C++的标准I/O流库时,必须包含以下两个头文件:

#include <iostream>
#include <iomanip>

iostream文件提供基本的输入/输出功能,iomanip文件提供格式化的功能。标准的输出流(通常是屏幕)称为cout,标准的输入流(通常是键盘)称为cin。

在数据类型方面,C++相对于C增加了bool类型,其值为true或false。整型值可赋值给bool变量,但会被隐式转换成true或false,当整型值非0时,转换为true;整型值为0时,转换为false。同样,bool值也可赋值给整型变量,当bool值为true时,转换为1;bool值为false时,转换为0。bool值也可插入到输出流cout,当bool值为true时,在屏幕上输出1;当bool值为false时,在屏幕上输出0。(P.S.其实在C99标准中,定义了bool类型,但要引入头文件<stdbool.h>)

在函数方面,C++引入了内联函数函数的重载。所谓内联函数是指定义函数时,在“返回类型 函数名(参数列表)”之前加上inline关键字,即“inline 返回类型 函数名(参数列表)”。对于内联函数,编译器是将其函数体放在调用该内联函数的地方,不存在普通函数调用时栈记录的创建和释放开销。使用内联函数时应注意以下几个问题:

  1. 在一个文件中定义的内联函数不能在另一个文件中使用,它们通常放在头文件中共享。
  2. 内联函数应该简洁,只有几个语句,如果语句较多,不适合定义为内联函数。
  3. 内联函数体中不能有循环语句、if语句或switch语句,否则函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。
  4. 内联函数要在函数被调用之前声明。

另外,在C++中,当有一组函数完成相似功能时,函数名允许重复使用,编译器根据参数表中参数的个数或类型来判断调用哪一个函数,这就是函数的重载。对于重载函数,只要其参数表中参数个数或类型不同,就视为不同的函数。定义重载函数时,应该注意以下几个问题:

  1. 避免函数名字相同,但功能完全不同的情形。
  2. 函数的形参变量名不同不能作为函数重载的依据。
  3. C++中不允许函数名相同、形参个数和类型也相同而返回值不同的情形,否则编译时会出现函数重复定义的错误。
  4. 调用重载的函数时,如果实参类型与形参类型不匹配,编译器会自动进行类型转换。如果转换后仍然不能匹配到重载的函数,则会产生一个编译错误。

在指针的应用方面,C++通过newdelete运算符分别用于堆中内存块的分配和释放。new和delete都是单目运算符,new的操作数是一个数据类型,结果是为该类型的变量分配的内存块首地址;delete运算符用于释放由new运算符分配的内存块,其操作数是指针,含义是释放该指针所指向的内存块,注意,当被释放的内存块是用new得到的数组时,需要添加[],如果指针指向的内存块不是用new申请的,则用delete释放时会产生一个严重的运行错误,如果指针为空时,它不指向任何内存单元,则释放它没有意义,也不会导致程序出错。

另一方面,C++中定义了“引用”这个概念,引用是一个变量的别名,除用“&”取代“*”外,定义引用的方法与定义指针类似。引用提供了与指针相同的能力,但比指针更为直观,更易于理解。引用可用常量来初始化,此时,常量引用作为另一个变量的别名用处不是很大,除非变量名特长。引用最重要的用处是作函数的参数,表示引用传递方式。

类与对象

C++与C最大的不同就是引入了面向对象编程(OOP:Object-Oriented Programming)的思想,现实世界中任何一个独一无二的实体都可以看成是对象,对象具有状态行为,对象的状态由数据成员(也称为“属性”)及其当前值构成,对象的行为由一组函数定义。对象的抽象是,类的实例化是对象,因此类也具有状态和行为。C++中类定义的一般形式如下:

class Name{
    public:
        类的公有接口
    private:
        私有的成员函数
        私有的数据成员定义 
};

类的定义由类头类体两部分组成,类头由关键字class开头,然后是类名,其命名规则与一般标识符的命名规则一致,类体放在一对花括号中,类的定义也是一个语句,所以要有分号结尾,否则会产生编译错误。类体定义类的成员,它支持如下两种类型的成员:

  1. 数据成员:它们指定了该类对象的内部表示。
  2. 成员函数:它们指定了该类的操作。

类成员有如下三种不同的访问权限:

  1. 公有(public):成员可以在类外访问。
  2. 私有(private):成员只能被该类的成员函数访问。默认的。
  3. 保护(protected):成员只能被该类的成员函数和派生类的成员函数访问。

数据成员通常是私有的;成员函数通常有一部分是公有的,一部分是私有的。公有的成员函数可在类外被访问,也称之为类的接口。我们来看下面这个栈结构的类Stack:

const int STACK_SIZE = 100;
class Stack{
    int top;             //数据成员:栈顶指针 
    int buffer[STACK_SIZE];  //数据成员:栈空间 
public:
    Stack()  //构造函数,后面将会提到
    {
        top = 0;
    }
    int length()  //成员函数:返回栈中元素的数目 
    {
        return top;
    }
    bool push(int element)  //成员函数:元素element入栈 
    {
        if(top == STACK_SIZE)
        {
            cout << "Stack is overflow!\n";
            return false;
        }
        else
        {
            buffer[top] = element;
            top++;
            return true;
        }
    }
    bool pop(int &e);
};

类的成员函数通常在类外定义,一般形式如下:

返回类型 类名::函数名(形参表)
{
    函数体 
}

双冒号“::”是域运算符,主要用于类的成员函数的定义,现在我们在类外定义上面类Stack中的成员函数pop():

bool Stack::pop(int &e)
{
    if(top == 0)
    {
        cout << "Stack is empty!\n";
        return false;
    }
    else
    {
        e = buffer[top-1];
        top–;
        return true;
    }
}

考虑到C到C++过渡的连续性,在C++中,允许在结构(struck)和联合(union)中定义函数,它们也具有类的功能。与class不同的是,结构和联合成员的默认访问控制为public。一般情况下,应该用class来描述面向对象概念中的类。定义了类以后,就可以定义类类型的变量,类的变量称为对象。例如:

Stack s1;      //创建一个Stack类的对象
Stack s2[10];   //创建由对象数组表示的10个Stack类对象

在所有函数之外定义的对象称之为全局对象,在函数体(或复合语句内)定义的对象称之为局部对象,在类中定义的对象称为成员对象。全局对象和局部对象的生存期和作用域的规定与普通变量相同。成员对象将随着包含它的对象的创建而创建、消亡而消亡,成员对象的作用域为它所在的类。通过new操作创建的对象称之为动态对象,其存储空间在内存的堆区。动态对象用delete操作撤销。例如:

Stack *p;
p = new Stack;
delete p;

我们可以通过“对象名.成员函数名(实参表)”或“指向对象的指针->成员函数名(实参表)”的形式来给对象进行操作。程序运行时创建的每个对象只有在初始化后才能使用,C++中定义了一种特殊的初始化函数,称之为构造函数。构造函数有一些独特的地方:函数的名字与类名相同,它没有返回类型和返回值。当对象创建时,会自动调用构造函数进行初始化。构造函数也可以重载,其中,不带参数(或所有参数都有默认值)的构造函数称为默认构造函数。对于常量数据成员和引用数据成员(某些静态成员除外),不能在声明时进行初始化,也不能采用赋值操作对它们进行初始化,但可以在定义构造函数时,在函数头和函数体之间加上一个对数据成员进行初始化的表来实现。例如:

class A{
    int x;
    const int y = 10;  //错误 
    int &z = x;  //错误 
public:
    A():z(x),y(10)  //数据成员初始化表 
    {
        x = 0;  //x初始化为0的处理也可放在初始化表中 
    }
    …
};

同理,和构造函数类似,当对象消亡时,往往也需要执行一些操作,例如归还对象占有的空间,这时,会自动调用析构函数进行一些清理工作。析构函数也与类同名,但在名字前有一个“~”,析构函数也没有返回类型和返回值,另外,析构函数不带参数,不能重载。当用户未显示定义构造函数和析构函数时,编译器会隐式定义一个内联的、公有的构造函数和析构函数。默认的构造函数执行创建一个对象所需要的一些初始化操作,但它并不涉及用户定义的数据成员或申请的内存的初始化。有时,可能需要一个或多个公共的数据成员或成员函数能够被类的所有对象共享,这时我们可以定义静态(static)的数据成员或成员函数,只要在其定义前增加static关键字即可。另外,在静态成员函数中,仅能访问静态的数据成员,不能访问非静态的数据成员,也不能调用非静态的成员函数。公有的、静态的成员函数在类外的调用方式为:类名::成员函数名(实参表)。对于一个类的非静态成员函数,它如何知道自己是对哪一个对象进行操作呢?其实,每个成员函数都拥有一个this指针,this是一个形参、一个局部变量,存在于类的非静态成员函数中(仅能在类的成员函数中访问),局部于某一个对象,它指向调用该函数的对象。

在C++的一个类定义中,可以指定某个全局函数、某个其他类或某个其他类的成员函数来直接访问该类的私有(private)和保护(protected)成员,它们分别称为友元函数友元类友元类函数,通称为友元。例如:

class A{
    …
    friend void func();  //友元函数 
    friend class B;  //友元类 
    friend void C::f();  //友元类函数,假定f()是类C的成员函数
    …
};

友元的作用是提高面向对象程序设计的灵活性,是数据保护和对数据的存取效率之间的一种折衷方案。

继承与多态

代码复用是C++最重要的特点之一,它是通过类继承机制来实现的。继承的一般形式如下:

class 派生类名:访问权限 基类名
{
    //派生类的类体 
}

派生类与基类是有一定联系的,基类(或称为父类)描述一个事物的一般特征,而派生类(或称为子类)有比基类更丰富的属性和行为。如果需要,派生类可以可以从多个基类继承,也就是多重继承。通过继承,派生类自动得到了除基类私有成员以外的其他所有数据成员和成员函数,在派生类中可以直接访问,从而实现了代码的复用。

访问权限是访问控制说明符,它可以是public、private或protected,它控制基类的数据成员和成员函数在派生类中的访问方法。当访问说明符为public时,基类的公有成员变为派生类的公有成员,基类的保护成员变为派生类的保护成员;当访问说明符为protected时,基类的公有和保护成员均变为派生类的保护成员;而当访问说明符为private时,基类的公有和保护成员均变为派生类的私有成员。

派生类对象生成时,要调用构造函数进行初始化,其过程是:先调用基类的构造函数,对派生类中的基类数据进行初始化,然后再调用派生类自己的构造函数,对派生类的数据进行初始化工作。当然,在派生类中也可以更改基类的数据,只要它有访问权限。

 



发表评论

电子邮件地址不会被公开。 必填项已用*标注