C++极速快速入门篇

第一章:C++的特点与OO思想

    1.1.C++与C简比:

      C语言有一个优点,即它的速度可以很快。写出来的程序可以很精练、简单、小巧;但如果将C和C++相比较,C++就经常会为了解决某个问题绕一个大圈,所以代码量相对较大。C++有个绝对的优势,就是针对不同对象去做实例化,这就是所谓的OO思想。

    1.2.什么是OO思想

      在面对对象的思想中,任何事物都可以被看做一个对象。一个再复杂的模型结构都是由千千万万个对象组成。这是根本思想。而对于每个对象,抽象起来最终可以分为两个要素:属性和行为。地球上的每种动物、每种植物、空气、水、土壤等都是对象。在面对对象编程中,程序员不再面对一个个函数和变量,而是要放眼整体,面对一个个对象来看问题。 每个对象都是一个完整的独立的个体,它是由相关的属性和行为组合,与外界分隔。例如对于总公司而言,分公司好比“黑箱”,总公司不需干预分公司如何调配人员工作等,只需下达命令和指标。

    1.3.OO思想的特点

      1.封装:封装意味着把对象的属性和方法结合成一个独立的系统单位,并尽可能隐藏对象的内部细节。封装是面向对象思想描述的基础,从此程序员面对的就不再是许多复杂的函数和过程实现,而是少数具有行为能力的个体实例。

      2.抽象:抽象的过程是对具体问题进行概括的过程,是对一类公共问题进行统一描述的过程。为了使某些必要的信息得以顺利的交流,设计者必须制定一个抽象,就如同一个协议,一个得到所有参与活动的有效个体支持的协议。 例如面包房提供一个抽象——“订单”。

      3.继承:子类对象拥有与其基类相同的全部属性和方法,称为继承。

      4.多态:多态是指在基类中定义的属性和行为被子类继承后,可以具有不同的数据类型或者表现行为等特性。例如动物有一个行为定义为Move(),那么这些乌龟,鸟,兔子等子类继承下来后会根据自己的特性采取不同个性的Move()方式。

第二章:基本的输入输出

    2.1.cout 是一个输出流对象,它是”console out(控制台输出)“的缩写。是属于basic_ostream 类的对象。ostream 类在iostream头文件中定义。注意…… 什么是输出流?其实也就是一个概念,在C++中引入了很多类似的概念:例如数据可以从键盘流入程序,又可以从程序流向屏幕、打印机等~

    2.2.using namesapce std;这条指定带给我们一个全新的概念:名字空间。 就是C++标准库所使用的所有标识符(即类、函数、对象等的名称)都是在同一个特殊的名字空间(std)中来定义的。 如果我们没有使用这条指令,我们将需要使用std::cout 这样的语法来调用输出流对象。后面会详解。

    2.3.流对象 cin。这个对象的类型是istream,它知道如何从用户终端读取数据。 cin >> i;      cin 输入操作符又称为提取操作符,它一次从输入流对象cin 提取一个数据。 当用户进行键盘输入时,对应的字符将输入到操作系统的键盘缓冲区中。 这样,当用户点击键盘上的“Enter”键时,操作系统把键盘缓冲区的内容传输到cin 流的内部缓冲区,“>>”操作符随后从这个缓冲区提取需要的信息。cin可以连续从键盘读取想要的数据,以空格、tab或换行作为分隔符例如cin>>a>>b>>c。如果到达了文件尾或者提取操作符遇到一个非法值,这个返回值将是 false。 我们小举一个例子:如果我们试图把一个浮点数读取到一个 int 型变量中,当提取操作符遇到小数点时,它将使输入流对象置于一个错误的状态中,导致 cin 的值为 false。

    2.4.argc 与 argv[] :在程序中,main 函数有两个参数,整型变量 argc 和字符指针数组 argv[]。 argc 的含义是程序的参数数量,包含本身。 argv[] 的每个指针指向命令行的一个字符串。

    2.5.cin 对象有几个专门用来报告其工作情况的成员函数,它们将返回一个真/假值来表明cin 的状态。 eof():如果到达文件(或输入)末尾,返回true; fail():如果cin 无法工作,返回true; bad():如果cin 因为比较严重的原因(例如内存不足)而无法工作,返回true; good():如果以上情况都没发生,返回true。

第三章:函数重载

      所谓函数重载的实质就是用同样的名字再定义一个有着不同参数但有着同样用途的函数。可以是参数个数上的不同,也可以是参数数据类型上的不同!注意区分重载和覆盖(覆盖后边我们会讲到); 我们只能通过不同参数进行重载,但不能通过不同的返回值(尽管后者也是一种区别)。

第四章:复杂的数据类型

      整数、实数、字符、字符串、数组、指针和结构,向量(vector);结构(Structure)是一种由程序员定义的、由其他变量类型组合而成的数据类型。 定义一个结构的基本语法是:

 struct name 
{ 
    type  varName1; 
    type  varName2; 
    std::string name;
    std::string uid;
    char sex;          // F==Female, M==Male
    。。。。。。
 }; 

    传值/传址/引用:

    联合、枚举和类型别名:联合与结构有很多相似之处,联合也可以容纳多种不同类型值,但是它每次只能存储这些值中的一个。

union  mima
{
    unsigned long     birthday;
    unsigned short   ssn;
    char*   pet;
};
mima  mima_1;
mima_1.birthday = 19881301;
mima_1.pet = “Chaozai”;

    这个联合将把 “Chaozai” 存入 mima_1 联合的 pet 成员,并丢弃 birthday 成员里的值

枚举(enum)类型用来创建一个可取值列表:
enum  weekdays { Monday, Tuesday,  Wednesday, Thursday, Friday };
定义一个枚举类型之后,我们就可以像下面这样创建该类型的变量:
weekdays  today;
然后我们像下边的方式对他进行赋值:
today = Thursday;

    我们这里不需要使用引号,因为枚举值不是字符串。。。 编译器会按照各个枚举值在定义时出现的先后顺序把它们与 0 ~ n-1 的整数(n 是枚举值的总个数)分别关联起来。 使用枚举类型好处有两个: 它们对变量的可取值加以限制; 它们可以用做 switch 条件语句的 case 标号。 (因为字符串是不能作为标号用的!小技巧哦~)

    Typedef 保留字,使用它可以为一个类型定义创建一个别名。 例如,我们不喜欢使用 int* 来创建指针,可以像下边这样定义一个类型别名: typedef  int*  intPointer; 在此之后,我们就可以像下面这样来定义整型指针了: intPointer  myPointer;

第五章:面向过程基础

    5.1.对象

      使用对象进行编程是C++的核心,也是我们常说的C++比C“高级”的重要根据之一。对象的本质上不过是一种新的数据类型,只不过它拥有无限的潜力。对于初学者而言,先给一个简单区别:对象的内部可以有变量和函数,而结构通常只由各种变量构成(c语言结构体内部可以使用函数指针方式)。举个例子看出差别:

#C语言结构体中引入函数
#include <stdio.h>

struct student {
	char *name;
	int age;

	void (*printInfo)(struct student *stu);
};

void printInfo(struct student *stu)
{
	printf("name = %s, age = %d", stu->name, stu->age);
}

int main(int argc, char**argv)
{
	struct student students[] = {
		{"zhangsan", 10, printInfo},
		{"lisi", 26, printInfo},
	};

	students[0].printInfo(&students[0]);
	students[1].printInfo(&students[1]);
}
#C++中结构体引入
#include <stdio.h>

struct student {
	char *name;
	int age;

	void printInfo(void)
	{
		printf("name = %s, age = %d\n", name, age);
	}
};

int main(int argc, char**argv)
{
	struct student students[] = {
		{"zhangsan", 10},
		{"lisi", 26},
	};

	students[0].printInfo();
	students[1].printInfo();
}
#C++中类引入
#include <stdio.h>

class student {
public:
	char *name;
	int age;

	void printInfo(void)
	{
		printf("name = %s, age = %d\n", name, age);
	}
};

int main(int argc, char**argv)
{
	student students[] = {
		{"zhangsan", 10},
		{"lisi", 26},
	};

	students[0].printInfo();
	students[1].printInfo();
}


      类(Class)就像是一副蓝图,它决定一个对象将是什么样的(具备什么样的属性、功能)。 所以OOP过程的第一步是创建一个类,而每个类跟变量一样都有一个名字:类名的第一个字母采用大写是一种习惯的上的标准,但不是硬性规定。还有在类声明末尾,必须有一个分号,这一点跟C++结构情况相同。类由变量和函数组成,对象将使用那些变量来存储信息,调用那些函数来完成操作。所以人们常常会看到一些专门术语:类里边的变量成为属性,函数成为方法。举个例子:

class Car
{
  public:
    std::string  color;
    std::string  engine;
    float  gas_tank;
    unsigned int  Wheel;

    void  fill_tank( float  liter );  
// 方法的声明:方法是”加油”,参数是”公升”
}
方法的定义通常安排在类声明的后面:
void  Car::fillTank(float  liter)
{
    gas_tank  +=  liter;
}

        作用域解析操作符(::),作用是告诉编译器这个方法存在于何处,或者说是属于哪一个类。事实上std::cout所引用的是std里定义的cout,而std::string数据类型其实也是一个对象。

C++允许在类里声明常量,但不允许对它进行赋值
class Car
{
public:
	const float TANKSIZE  =  85;  // 出错@_@
}
绕开这一限制的方法就是创建一个静态常量
class Car
{
public:
    static const float FULL_GAS = 85;
}

    5.2.构造器与析构器

      之前我们讨论了使用面向对象编程技术开发程序最基本步骤: 定义一个有属性和方法的类(模板) 为该类创建一个变量(实现);接下来介绍一些更复杂和更有用的概念。

     首先是构造器,它是类里的一种特殊的方法。构造器和通常方法的主要区别: 1:构造器的名字必须和它所在的类的名字一样2:系统在创建某个类的实例时会第一时间自动调用这个类的构造器 3:构造器永远不会返回任何值;

     创建构造器,需要先把它的声明添加到类里:

class Car {
    Car( void );
}

     定义构造器:

#注意大小写与类名保持一致。在结束声明之后开始定义构造器本身:

Car::Car(void)   // 不用写 void Car::Car(void)
{
    color = “WHITE”;
    engine = “V8”;
    wheel = 4;
    gas_tank = FULL_GAS;
}

     每个类至少有一个构造器,如果你没有在类里定义一个构造器,编译器就会使用如下语法替你定义一个:ClassName::ClassName() { } 这是一个没有代码内容的空构造器,除此之外,编译器还会替你创建一个副本构造器(CopyConstructor)。(这个迟些给大家介绍)

     然后是析构器:一般来说,构造器用来完成事先的初始化和准备工作(申请分配内存),析构器用来完成事后所必须的清理工作(清理内存)。首先,析构器有着和构造器/类一样的名字,只不过前边多了一个波浪符“~”前缀。 class Car { Car(void); ~Car(); };其次,析构器也永远不返回任何值。 另外,析构器是不带参数的。所以析构器的声明永远是如下格式:~ClassName();

    5.3:类的继承

      先说thsi指针:在对象的世界里,有一个特殊的指针,它叫做this。我们通过一个典型的例子来认识它:

class Human {
char  fishc;
Human(char  fishc);
}
Human::Human(char  fishc){
	fishc = fishc;
}

      在”fishc = fishc”之前,所有的语法都没有任何问题: Human()构造器有一个名为fishc的参数 虽然他与Human类里边的属性同名,但却是不相干的两样东西,所以并没有错。 可是,问题是怎样才能让构造器知道哪个是参数,哪个是属性呢? 这时候,就需要用到他了 – this指针。 this -> fishc = fishc;这样修改之后,编译器就知道,赋值操作符的左边将被解释为当前对象的fishc属性,右边将被解释为构造器的传入来的fishc参数。 注意: 使用this指针的基本原则是:如果代码不存在二义性隐患,就不必使用this指针!

      再说继承:继承机制使得程序员可以创建一个类的堆叠层次结构,每个子类均将继承在它的基类里定义的方法和属性。 简单地说,通过继承机制,程序员可以对现有的代码进行进一步的扩展,并应用在新的程序中。

      假设我们有一只乌龟和一只猪,它们都有一些共同特征:例如都有嘴巴会吃东西,都睡觉,都看到美女会流口水云云。 当然,它们也有不同的地方:例如乌龟会游泳,猪会爬树。。。。。。 我们说回编程,那要如何才能把这些变成C++的类呢?

      基类和子类:那么我们就需要编写一个Animal类作为Turtle类和Pig类的基类。 基类: 基类是可以派生出其他的类,也称为父类或超类。比如这里的Animal类是基类。 子类: 子类是从基类派生出来的类,比如这里的Turtle类和Pig类是子类。

      那么Animal类就拥有了Turtle类和Pig类的共同特征:吃东西、睡觉、流口水。 这里我们把这些动作都抽象化为方法 eat(), sleep(), drool(); 代表吃东西、睡觉、流口水的eat(), sleep(), drool() 是Animal类里的方法,因为每只动物都会做这些动作。 而swim() 方法在Turtle类里实现,climb() 方法在Pig类里实现。

#include <iostream>
#include <string>

class Animal
{
public:
    std::string mouth;

    void eat();
    void sleep();
    void drool();
};

class Pig : public Animal
{
public:
    void climb();
};

class Turtle : public Animal
{
public:
    void swim();
};

void Animal::eat()
{
    std::cout << "I'm eatting!" << std::endl;
}

void Animal::sleep()
{
    std::cout << "I'm sleeping!Don't disturb me!" << std::endl;
}

void Animal::drool()
{
    std::cout << "我是动物,你也是动物,我们都在流口水。。。" << std::endl;
}

void Pig::climb()
{
    std::cout << "我是一个只漂亮的小猪,我会上树,我正在爬树我最近涨价了。。" << std::endl;
}

void Turtle::swim()
{
    std::cout << "我是一只小鱼,我要游到海里。。" << std::endl;
}

int main()
{
    Pig pig;
    Turtle turtle;

    pig.eat();
    turtle.eat();
    pig.climb();
    turtle.swim();

    return 0;
}

      说下继承机制下的构造器与析构器调用顺序:根据基类必须在子类之前初始化原则!整体顺序为:基类

    5.4.访问控制

      前言:在 Human 类里有一个 swim() 方法,假如在设计一条鱼的时候,就用 Human 去派生一个 Fish 类。 从技术角度讲,这麽做没有问题,但像这样的运用继承机制实在太牵强,所以应保持基类和子类之间的关系应该自然和清晰的原则!

      在此前的例子里,我们无论是Animal, Pig 和 Turtle 类的所有成员都是用 public: 语句声明。 所谓访问控制,就是C++ 提供了一种用来保护类里的方法和属性的手段。 这里所说的保护意思是对谁可以调用某个方法和访问某个属性加上一个限制。如果某个对象试图调用一个它无权访问的函数,编译器将报错。 我们看下C++中的访问级别:

级别

允许谁来访问

public

任何代码

protected

这个类本身和它的子类

private

只有这个类本身

class Animal
{
public:
    std::string name;

    Animal(std::string theName);
    void eat();
    void sleep();
    void drool();
}; 
#请看 name 属性的访问级别是 public,这就意味着任何代码都可以改变它的值。

      就像我们不能随便改变一个人的身份证的名字一样,Animal 类里的 name 属性应该受到保护。

      在编写你的类定义代码时,应该从 public: 开始写起,然后是 protected:, 最后是 private:。 虽然编译器并不挑剔这些顺序,但这么做的好处是 —— 当你想知道某个特定的类提供了哪些方法和属性时,好的顺序可以为你节省大量的时间!

      说一下友元关系:一个完全无关的类由于某些特殊原因需要访问到某个 protected 成员,甚至某个 private 成员,那该怎么办呢? 有些朋友可能会说,那就把所有的东西都声明成 public 吧。这样固然可以,但这样不就把原来我们想方设法要保护的方法或属性又暴漏了吗?!友元关系用于解决此类问题!

      友元关系是类之间的一种特殊关系,这种关系不仅允许友元类访问对方的 public 方法和属性,还允许友元访问对方的 protected 和 private 方法和属性。声明一个友元关系的语法很简单,只要在类声明里的某个地方加上一条 friend class 类名 就行了。 注:这条语句可以放在任何地方,放在 public, protected, private 段落里都可以。举个栗子:

class Lovers
{
public:
    Lovers(std::string theName);
    void kiss(Lovers *lover);
    void ask(Lovers *lover, std::string something);

protected:
    std::string name;

    friend class Others;    
};

class Others
{
public:
    Others(std::string theName);
    void kiss(Lovers *lover);

protected:
    std::string name;
};

#则Others类能访问到Lovers的protected中成员

    5.5.覆盖方法和重载方法

      子类重写基类中的方法叫覆盖,也叫方法重写。我们需要做的是在类里重新声明这个方法,然后再改写一下它的实现代码。

      重载机制使你可以定义多个同名的方法(函数),只是它们的输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法) 重载并不是一个真正面向对象特性,它只是可以简化编程工作的捷径,而简化编程工作正式 C++ 的全部追求!

      对方法(函数)进行重载一定要有的放矢,重载的方法(函数)越多,程序就越不容易看懂。 在对方法进行覆盖(注意区分覆盖和重载)时一定要看仔细,因为只要声明的输入参数和返回值与原来的不一致,你编写出来的就将是一个重载方法而不是覆盖方法。关于返回值的区别如下:

      重写(override)方法原则:

            子类的方法的名称必须和所覆盖的方法相同

            子类的方法的参数必须和所覆盖的方法相同

            子类的返回类型必须和所覆盖的方法相同 (除过子类中方法的返回类型是父类中返回类型的子类)

            子类抛出异常小于等于父类方法抛出异常

            子类访问权限大于等于父类方法访问权限

      重载(overload)方法原则:

            方法名必须相同

            方法的参数列表不相同(包括参数类型,参数个数,参数顺序)

            方法的返回类型和方法的修饰符可以不相同

      总结:方法重载的返回值类型可以不相同,方法重写的返回值必须相同,否则编译报错。

第六章:高级方法

    6.1.静态属性和静态方法

      面对对象编程技术的一个重要特征是用一个对象把数据和对数据处理的方法封装在一起。 大家还记得,在前边的例子里我们一直是在使用对象(也可以说某个类的实例)来调用方法,每个方法只处理调用它的那个对象所包含的数据,所有的数据都属于同一个对象。 这就引发了一个问题:如果我们所需要的功能或数据不属于某个特性的对象,而是属于整个类的,如果我们坚决不建议在非必要的时候声明全局变量。该怎么办?

      这个问题必须使用 C++ 的静态属性和静态函数才能完美地得到解决。 C++ 允许我们把一个或多个成员声明为属于某个类,而不是仅属于该类的对象。另外一个好处是能够让有关的数据仍在该类的所有对象间共享。 创建一个静态属性和静态方法: 只需要在它的声明前加上 static 保留字即可。

class Pet
{
public:
    Pet(std::string theName);
    ~Pet();

    static int getCount();//静态成员函数

protected:
    std::string name;

private:
    static int count;//静态成员
};

int Pet::count = 0;         // 注意这一句:他起码做了两件事

      语法: 静态成员是所有对象共享的,所以不能在静态方法里访问非静态的元素。 非静态方法可以访问类的静态成员,也可以访问类的非静态成员。

      对象与对象之间的成员变量是相互独立的。要想共用数据,则需要使用静态成员和静态方法。

   只要在类中声明静态成员变量,即使不定义对象,也可以为静态成员变量分配空间,进而可以使用静态成员变量。(因为静态成员变量在对象创建之前就已经被分配了内存空间)

  静态成员变量虽然在类中,但它并不是随对象的建立而分配空间的,也不是随对象的撤销而释放(一般的成员在对象建立时会分配空间,在对象撤销时会释放)。静态成员变量是在程序编译时分配空间,而在程序结束时释放空间。

 静态成员的定义和声明要加个关键static。静态成员可以通过双冒号来使用,即<类名>::<静态成员名>。

 初始化静态成员变量要在类的外面进行。初始化的格式如下:数据类型  类名::静态成员变量名 = 初值;

 不能用参数初始化表,对静态成员变量进行初始化。

 既可以通过类名来对静态成员变量进行引用,也可以通过对象名来对静态成员变量进行引用。

  普通成员函数和静态成员函数的区别是:普通成员函数在参数传递时编译器会隐藏地传递一个this指针.通过this指针来确定调用类产生的哪个对象;但是静态成员函数没有this指针,不知道应该访问哪个对象中的数据,所以在程序中不可以用静态成员函数访问类中的普通变量.

       回顾下:this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象的地址。 当一个对象被创建时,该对象的 this指针就自动指向对象数据的首地址。

#include<iostream>

class Point
{
private:
  int x, y;
public:
  Point(int a, int b)
	{ 
		x = a;
		y = b;
	}
  void MovePoint( int a, int b)
	{ 
		x = a; 
		y = b;
	}
  void print()
	{ 
		std::cout << "x=" << x << "y=" << y << endl;
	}
};

int main()
{
  Point point1(10, 10);
  point1.MovePoint(2, 2);
  point1.print();

	return 0;
}

// 当对象point1调用MovePoint(2,2)函数时,即将point1对象的地址传递给了this指针。

// MovePoint函数的原型事实上应该是 void MovePoint( Point *this, int a, int b);
// 第一个参数是指向该类对象的一个指针,我们在定义成员函数时没看见是因为这个参数在类中是隐含的。

// 这样point1的地址传递给了this,所以在MovePoint函数中便可以显式的写成:void MovePoint(int a, int b) { this->x = a; this->y = b;} 
// 即可以知道,point1调用该函数后,也就是point1的数据成员被调用并更新了值。

       在程序运行时,对象的属性(变量)和方法(函数)都是保存在内存里,这就意味着它们各自都有与之相关联的地址。 这些地址都可以通过指针来访问,而 this指针毋庸置疑是保存着对象本身的地址。

       每当我们调用一个方法的时候,this指针都会随着你提供的输入参数被秘密的传递给那个方法。 正是因为如此,我们才能在方法里像使用一个局部变量那样使用 this指针。 因为静态方法不是属于某个特定的对象,而是由全体对象共享的,这就意味着它们无法访问 this指针。所以,我们才无法在静态方法里访问非静态的类成员。

       在使用静态属性的时候,千万不要忘记为它们分配内存。具体做法很简单,只要在类声明的外部对静态属性做出声明(就像声明一个变量那样)即可。

    6.2.虚方法

       先介绍两个新的C++保留字:new和delete.在C和C++中,我们完全可以在没有创建变量的情况下为有关数据分配内存。也就是直接创建一个指针并让它指向新分配的内存块:

int *pointer = new int;
*pointer = 110;
std::cout << *pointer;
delete pointer;

       最后一步非常必要和关键,因为程序不会自动释放内存,程序中每一个 new 操作都必须有一个与之对应的 delete 操作!

       再说虚方法:来预测这个程序的输出.

#include <iostream>
#include <string>

class Pet
{
public:
	Pet(std::string theName);

	void eat();
	void sleep();
	void play();

protected:
	std::string name;
};

class Cat : public Pet
{
public:
	Cat(std::string theName);

	void climb();
	void play();
};

class Dog : public Pet
{
public:
	Dog(std::string theName);

	void bark();
	void play();
};

Pet::Pet(std::string theName)
{
	name = theName;
}

void Pet::eat()
{
	std::cout << name << "正在吃东西!\n";
}

void Pet::sleep()
{
	std::cout << name << "正在睡大觉!\n";
}

void Pet::play()
{
	std::cout << name << "正在玩儿!\n";
}

Cat::Cat(std::string theName) : Pet(theName)
{
}

void Cat::climb()
{
	std::cout << name << "正在爬树!\n";
}

void Cat::play()
{
	Pet::play();
	std::cout << name << "玩毛线球!\n";
}

Dog::Dog(std::string theName) : Pet(theName)
{
}

void Dog::bark()
{
	std::cout << name << "旺~旺~\n";
}

void Dog::play()
{
	Pet::play();
	std::cout << name << "正在追赶那只猫!\n";
}

int main()
{
	Pet *cat = new Cat("加菲");
	Pet *dog = new Dog("欧迪");

	cat -> sleep();
	cat -> eat();
	cat -> play();

	dog -> sleep();
	dog -> eat();
	dog -> play();

	delete cat;
	delete dog;

	return 0;
}

     哈哈!执行正常!

   

       但实际上可能出现这样的情况:程序与我们的预期不符:我们在 Cat 和 Dog 类里对 play() 方法进行了覆盖,但实际上调用的是 Pet::play() 方法而不是那两个覆盖的版本。why?

       程序之所以会有这样奇怪的行为,是因为C++的创始者希望用C++生成的代码至少和它的老前辈C一样快。 所以程序在编译的时候,编译器将检查所有的代码,在如何对某个数据进行处理和可以对该类型的数据进行何种处理之间寻找一个最佳点。 正是这一项编译时的检查影响了刚才的程序结果:cat 和 dog 在编译时都是 Pet 类型指针,编译器就认为两个指针调用的 play() 方法是 Pet::play() 方法,因为这是执行起来最快的解决方案。

       而引发问题的源头就是我们使用了 new 在程序运行的时候才为 dog 和 cat 分配 Dog 类型和 Cat 类型的指针。 这些是它们在运行时才分配的类型,和它们在编译时的类型是不一样的! 为了让编译器知道它应该根据这两个指针在运行时的类型而有选择地调用正确的方法(Dog::play() 和 Cat::play()),我们必须把这些方法声明为虚方法。

       声明一个虚方法的语法非常简单,只要在其原型前边加上 virtual 保留字即刻。 virtual void play(); 另外,虚方法是继承的,一旦在基类里把某个方法声明为虚方法,在子类里就不可能再把它声明为一个非虚方法了。

       小结:1.如果拿不准要不要把某个方法声明为虚方法,那麽就把它声明为虚方法好了。2.在基类里把所有的方法都声明为虚方法会让最终生成的可执行代码的速度变得稍微慢一些,但好处是可以一劳永逸地确保程序的行为符合你的预期!3.在实现一个多层次类继承关系的时候,最顶级的基类应该只有虚方法。4.析构器都是虚方法!从编译的角度看,它们只是普通的方法。如果它们不是虚方法,编译器就会根据它们在编译时的类型而调用那个在基类里定义的版本(构造器),那样往往会导致内存泄露!

    6.3.抽象方法

       抽象方法(abstract method,也可以成为纯虚函数)是面向对象编程技术的另一个核心概念,在设计多层次的类继承关系时常会用到。 把某个方法声明为一个抽象方法等于告诉编译器这个方法必不可少,但现在(在基类里)还不能为它提供一个实现!抽象方法的语法很简单:在声明一个虚方法的基础上,在原型的末尾加上”=0”。(告诉编译器不用浪费时间在这个类里寻找这个方法的实现!)

       多态性是面向对象程序设计的重要特征之一。 简单的说,多态性是指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作,从而实现传说中的”一个接口,多种方法”! 多态是如何实现绑定的? 编译时的多态性:通过重载实现 运行时的多态性:通过虚函数实现编译时的多态性特点是运行速度快,运行时的特点是高度灵活和抽象。

       析构函数解析:如果我们把类 ClxBase 析构函数前的 virtual 去掉,那输出结果就应该是: Do something in class ClsDerived!  out of base!也就是说子类 ClxDerived 的析构函数没有被调用!而加上virtual,则依次调用子类->基类析构函数!

#include <iostream>

class ClxBase
{
public:
    ClxBase()
    {
    };

    ~ClxBase()
    {
        std::cout << "out of base!\n";
    };

    virtual void doSomething()
    {
        std::cout << "Do something in class ClxBase!\n";
    }
};

class ClxDerived : public ClxBase
{
public:
    ClxDerived()
    {
    };

    ~ClxDerived()
    {
        std::cout << "Output from the destructor of class ClxDerived!\n";
    };

    void doSomething()
    {
        std::cout << "Do something in class ClxDerived!\n";
    };
};

int main()
{
    ClxBase *pTest = new ClxDerived;

    pTest -> doSomething();

    delete pTest;

    return 0;
}

       一般子类的析构函数里面都是释放内存资源,而析构函数不被调用的话旧会造成内存泄露。 所以,析构器都是虚方法是为了当一个基类的指针删除一个派生类的对象时,派生类的析构函数可以被正确调用。 当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里边存放着虚函数指针。为了节省资源,只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数!

    6.4.运算符重载

       所谓重载,就是重新赋予新的含义。函数重载是对一个已有的函数赋予新的含义,使之实现新功能。运算符重载是通过定义函数实现的。运算符重载实质上是函数的重载。

重载运算符的函数一般格式如下:
    函数类型  operator  运算符名称(形参表列)
{
    对运算符的重载处理
}
例如我们可以重载运算符 + , 如下:
int operator+(int a, int b)
{
    return (a – b);
}

举个栗子:实现复数加法。如(3, 4i)+ (5, -10i)= (8, -6i)

#include <iostream>

// 演示对运算符"+"进行重载达到目的!

class Complex
{
public:
    Complex();
    Complex(double r, double i);
    Complex operator+(Complex &d);
    void print();

private:
    double real;
    double imag;
};

Complex::Complex()
{
    real = 0;
    imag = 0;
}

Complex::Complex(double r, double i)
{
    real = r;
    imag = i;
}

Complex Complex::operator+(Complex &d)
{
    Complex c;

    c.real = real + d.real;
    c.imag = imag + d.imag;

    return c;
}

void Complex::print()
{
    std::cout << "(" << real << ", " << imag << "i)\n";
}

int main()
{
    Complex c1(3, 4), c2(5, -10), c3;

    c3 = c1 + c2;

    std::cout << "c1 = ";
    c1.print();
    std::cout << "c2 = ";
    c2.print();
    std::cout << "c1 + c2 = ";
    c3.print();

    return 0;
}

       一些运算符重载规则:

C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载。
除了一下五个阿哥不允许重 .(成员访问运算符)
 .*(成员指针访问运算符)
 ::(域运算符)
载外,其他运算符允许重载:
 sizeof(尺寸运算符)
 ?:(条件运算符)

重载不能改变运算符运算对象(操作数)个数。
重载不能改变运算符的优先级别。
重载不能改变运算符的结合性。
重载运算符的函数不能有默认的参数。
重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应该有一个是类对象或类对象的引用。

(也就是说,参数不能全部都是C++的标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符性质)

       那么可能有这样的疑问:”+”运算符是双目运算符,为什么刚刚的例子中的重载函数只有一个参数呢? 解答:实际上,运算符重载函数有两个参数,但由于重载函数是 Complex 类中的成员函数,有一个参数是隐含着的,运算符函数是用 this 指针隐式地访问类对象的成员。

return Complex(real+c2.real, imag+c2.imag);
return Complex(this->real+c2.real, this->imag+c2.imag);
return Complex(c1.real+c2.real, c1.imag+c2.imag);

       例子中的 c1 + c2,编译系统把它解释为: c1.operator+(c2) 即通过对象 c1 调用运算符重载函数,并以表达式中第二个参数(运算符右侧的类对象c2)作为函数实参。 运算符重载函数除了可以作为类的成员函数外,还可以是非成员函数:放在类外,做 Complex 类的友元函数存在。

#include <iostream>

// 演示对运算符"+"进行重载达到目的!

class Complex
{
public:
    Complex();
    Complex(double r, double i);
    friend Complex operator+(Complex &c, Complex &d);
    void print();

private:
    double real;
    double imag;
};

Complex::Complex()
{
    real = 0;
    imag = 0;
}

Complex::Complex(double r, double i)
{
    real = r;
    imag = i;
}

// 注意,这里作为友元函数,不属于Complex,记得别写 :: 咯!
Complex operator+(Complex &c, Complex &d)
{
    return Complex(c.real+d.real, c.imag+d.imag);
}

void Complex::print()
{
    std::cout << "(" << real << ", " << imag << "i)\n";
}

int main()
{
    Complex c1(3, 4), c2(5, -10), c3;

    c3 = c1 + c2;

    std::cout << "c1 = ";
    c1.print();
    std::cout << "c2 = ";
    c2.print();
    std::cout << "c1 + c2 = ";
    c3.print();

    return 0;
}

       为什么把运算符函数作为友元函数呢? 因为运算符函数要访问 Complex 类对象的成员,如果运算符函数不是 Complex 类的友元函数,而是一个普通的函数,它是没有权力访问 Complex 类的私有成员的。 由于友元的使用会破坏类的封装,因此从原则上说,要尽量将运算符函数作为成员函数。

    6.5.多继承与虚继承

       什么时候需要用到多继承(multiple inheritance)?只要你遇到的问题无法只用一个”是一个”关系来描述的时候!

       举个栗子:在学校里有老师和学生,他们都是人(Person),我们可以用”老师是人”和”学生是人”语法来描述这种情况。 从面相对象编程角度上来看,我么应该创建一个名为 Person 的基类和两个名为 Teacher 和 Student 的子类,后两者是从前者继承来的。

       问题来了:有一部分学生还教课挣钱(助教),该怎么办?酱紫就存在了既是老师又是学生的复杂关系,也就是同时存在着两个”是一个”关系。 我们需要写一个 TeschingStudent 类让它同时继承 Teacher 类和 Student 类,换句话说,就是需要使用多继承。 基本语法: class TeachingStudent : public Student, public Teacher { … }

       

#include <iostream>
#include <string>

class Person
{
public:
    Person(std::string theName);

    void introduce();

protected:
    std::string name;
};

class Teacher : public Person
{
public:
    Teacher(std::string theName, std::string theClass);

    void teach();
    void introduce();

protected:
    std::string classes;
};

class Student : public Person
{
public:
    Student(std::string theName, std::string theClass);

    void attendClass();
    void introduce();

protected:
    std::string classes;
};

class TeachingStudent : public Student, public Teacher
{
public:
    TeachingStudent(std::string theName, std::string classTeaching, std::string classAttending);

    void introduce();
};

Person::Person(std::string theName)
{
    name = theName;
}

void Person::introduce()
{
    std::cout << "大家好,我是" << name << "。\n\n";
}

Teacher::Teacher(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}

void Teacher::teach()
{
    std::cout << name << "教" << classes << "。\n\n";
}

void Teacher::introduce()
{
    std::cout << "大家好,我是" << name << ", 我教" << classes << "。\n\n";
}

Student::Student(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}

void Student::attendClass()
{
    std::cout << name << "加入" << classes << "学习。\n\n";
}

void Student::introduce()
{
    std::cout << "大家好,我是" << name << ", 我在" << classes << "学习。\n\n";
}

TeachingStudent::TeachingStudent(std::string theName,
                                 std::string classTeaching,
                                 std::string classAttending)
                                 : Teacher(theName, classTeaching), Student(theName, classAttending)
{
}

void TeachingStudent::introduce()
{
    std::cout << "大家好,我是" << Student::name << "。我教" << Teacher::classes << ", ";
    std::cout << "同时我在" << Student::classes << "学习。\n\n";
}

int main()
{
    Teacher teacher("小甲鱼", "C++入门班");
    Student student("迷途羔羊", "C++入门班");
    TeachingStudent teachingStudent("丁丁", "C++入门班", "C++进阶班");

    teacher.introduce();
    teacher.teach();
    student.introduce();
    student.attendClass();
    teachingStudent.introduce();
    teachingStudent.teach();
    teachingStudent.attendClass();

    return 0;
}

       在使用多继承的时候,要特别注意继承了基类的多少个副本。 在使用多继承的时候,最安全简明的做法是从没有任何属性且只有抽象方法的类开始继承。 这样可以让你远离后代子类可能拥有好几个基类属性的问题,这样的类又叫做接口(interface)。

        这个示例程序看起来似乎已经解决了问题,但它存在着一些隐患。 首先,在 TeachingStudent 类的 introduce() 方法里,我们不得不明确地告诉编译器应该使用哪一个属性。同时在 TeachingStudent 对象里可以继承两个不同的 classes 属性,那它是不是应该有两个不同的 name 属性呢? 答案:是!事实上,TeachingStudent 可以有两个不同的名字,违背了name的唯一性。

TeachingStudent::TeachingStudent(std::string theName1,
                                 std::string theName2,
                                 std::string classTeaching,
                                 std::string classAttending)
                                 : Teacher(theName1, classTeaching), Student(theName2, classAttending)
{
}
TeachingStudent teachingStudent("丁丁", "丹丹", "C++入门班", "C++进阶班");

       C++ 发明者也想到了这部分的冲突,因此为此提供了一个功能可以解决这个问题:虚继承(virtual inheritance) 通过虚继承某个基类,就是在告诉编译器:从当前这个类再派生出来的子类只能拥有那个基类的一个实例。 虚继承的语法: class Teacher : virtual public Person { … }。这样做我们的问题就解决了:让 Student 和 Teacher 类都虚继承自 Person 类,编译器将确保从 Student 和 Teacher 类再派生出来的子类只能拥有一份 Person 类的属性!

#include <iostream>
#include <string>

class Person
{
public:
    Person(std::string theName);

    void introduce();

protected:
    std::string name;
};

class Teacher : virtual public Person
{
public:
    Teacher(std::string theName, std::string theClass);

    void teach();
    void introduce();

protected:
    std::string classes;
};

class Student : virtual public Person
{
public:
    Student(std::string theName, std::string theClass);

    void attendClass();
    void introduce();

protected:
    std::string classes;
};

class TeachingStudent : public Student, public Teacher
{
public:
    TeachingStudent(std::string theName, std::string classTeaching, std::string classAttending);

    void introduce();
};

Person::Person(std::string theName)
{
    name = theName;
}

void Person::introduce()
{
    std::cout << "大家好,我是" << name << "。\n\n";
}

Teacher::Teacher(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}

void Teacher::teach()
{
    std::cout << name << "教" << classes << "。\n\n";
}

void Teacher::introduce()
{
    std::cout << "大家好,我是" << name << ", 我教" << classes << "。\n\n";
}

Student::Student(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}

void Student::attendClass()
{
    std::cout << name << "加入" << classes << "学习。\n\n";
}

void Student::introduce()
{
    std::cout << "大家好,我是" << name << ", 我在" << classes << "学习。\n\n";
}

TeachingStudent::TeachingStudent(std::string theName,
                                 std::string classTeaching,
                                 std::string classAttending)
                                 :
                                 Teacher(theName, classTeaching),
                                 Student(theName, classAttending),
                                 Person(theName)
{
}

void TeachingStudent::introduce()
{
    std::cout << "大家好,我是" << name << "。我教" << Teacher::classes << ", ";
    std::cout << "同时我在" << Student::classes << "学习。\n\n";
}

int main()
{
    Teacher teacher("小甲鱼", "C++入门班");
    Student student("迷途羔羊", "C++入门班");
    TeachingStudent teachingStudent("丁丁", "C++入门班", "C++进阶班");

    teacher.introduce();
    teacher.teach();
    student.introduce();
    student.attendClass();
    teachingStudent.introduce();
    teachingStudent.teach();
    teachingStudent.attendClass();

    return 0;
}

    6.6.动态内存分配

       静态内存和动态内存的区别不再赘述。动态内存由一些没有名字、只有地址的内存块构成,那些内存块是在程序运行期间动态分配的。C++标准库维护内存池。如果有足够的可用内存能满足你的申请,new 语句将返回新分配地址块的起始地址。如果没有足够的可用内存空间? 那么 new 语句将抛出 std::bad_alloc 异常! 注意在用完内存块之后,应该用 delete 语句把它还给内存池。另外作为一种附加的保险措施,在释放了内存块之后还应该把与之关联的指针设置为NULL。

       new 语句返回的内存块很可能充满”垃圾”数据,所以我们通常先往里边写一些东西覆盖,再访问它们,或者在类直接写一个构造器来初始化。

       注意:在重新使用某个指针之前千万不要忘记调用 delete 语句,如果不这样做,那个指针将得到一个新内存块的地址,而程序将永远也无法释放原先那个内存块,因为它的地址已经被覆盖掉了。 请记住,delete 语句只释放给定指针变量正指向的内存块,不影响这个指针。

       实现一个动态数组:注意删除的时候对于数组要使用delete [] x;

       动态内存的另一个常见用途是让函数申请并返回一个指向内存块的指针。掌握这个技巧很重要,尤其是在你打算使用由别人编写的库文件时。这个技巧的基本思路并不复杂:在函数里调用 new 语句为某种对象或某种基本数据类型分配一块内存,再把那块内存的地址返回给程序的主代码,主代码将使用那块内存并在完成有关操作后立刻释放。

       如果你想让一个函数在不会留下任何隐患的情况下返回一个指针,那它只能是一个动态分配的内存块的基地址。为什么不应该让函数返回一个指向局部变量的指针?因为局部变量在栈里,作用域限制。而new从堆上分配内存,需要手动释放,可作为返回值。

       同时应警惕避免内存泄露:new语句所返回的地址是访问这个内存块的唯一线索,同时也是delete语句用来把这个内存块归还给内存池的唯一线索。举个栗子

int *x;
x = new int[3000];
x = new int[4000];
delete[] x;
x = NULL;

       此时出现内存泄漏,再举个例子:

void foo()
{
My Class *x;
x = new MyClass();
}

       当foo()函数结束时,指针变量x将超出它的作用域,这意味着它将不复存在,它的值当然就会丢失。

有两种方法可以用来堵住这样的漏洞:

      第一个方法是在return语句之前的某个地方插入一条delete x语句:

void foo()
{
    MyClass *x;
    x = new MyClass();
    delete x;
    x = NULL;
    return;
}

       第二个方法是让函数把内存块的地址返回给它的调用者:

MyClass *foo()
{
    MyClass *x;
    x = new MyClass();

    return x;
}

       动态内存不存在作用域的问题,一旦被分配,内存块就可以在程序的任何地方使用。因为动态内存没有作用域,所以必须由程序员来跟踪它们的使用情况,并在不再需要用到它们的时候把它们及时归还给系统。这里需要特别注意的是,虽然动态分配的内存块没有作用域,但用来保存其地址的指针变量是受作用域影响的。

    6.7.副本构造器

       我们可以把一个对象赋值给一个类型与之相同的变量。 编译器将生成必要的代码把”源”对象各属性的值分别赋值给”目标”对象的对应成员。这种赋值行为称之为逐位复制(bitwise coyp)。 这在绝大多数场合都没有问题,但如果某些成员变量是指针,就存在问题:对象成员进行逐位复制的结果是你将拥有两个一摸一样的实例,而这两个副本里的同名指针会指向相同的地址。当删除其中一个对象时,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,就会出问题!

       例如分析下面几行代码:

MyClass  obj1;
MyClass  obj2;
obj2 = obj1;

       前两行代码很简明,它们创建了两个MyClass类实例obj1和obj2。第三行代码把obj1的值赋值给了obj2,这可能会导致问题!那么,怎样才能截获这个赋值操作并告诉它应该如何处理那些指针呢?案是对操作符进行重载!

       重载”=”操作符,在其中对指针进行处理: MyClass  &operator = (const  MyClass  &rhs); 上边的语句告诉我们这个方法所预期的输入参数应该是一个MyClass类型的、不可改变的引用。因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为它创建另外一个副本(否则可能导致无限递归);这里只读取参数得值,用const把那个引用声明为一个常量;

       但即使我们对赋值操作符进行了重载,由编译器创建的副本构造器仍以”逐位复制”方式把obj1赋值给obj2,所以要自定义一个副本构造器,而不是让系统帮我们生成.

MyClass( const MyClass &rhs);

这个构造器需要一个固定不变(const)的MyClass类型的引用作为输入参数,就像赋值操作符那样。因为他是一个构造器,所以不需要返回类型。构造器内可以为空。

#include <iostream>
#include <string>

class MyClass
{
public:
    MyClass(int *p);
    MyClass(const MyClass &rhs);
    ~MyClass();

    MyClass &operator=(const MyClass &rhs);
    void print();
private:
    int *ptr;
};

MyClass::MyClass(int *p)
{
    std::cout << "进入主构造器\n";
    ptr = p;
    std::cout << "离开主构造器\n";
}

MyClass::MyClass(const MyClass &rhs)
{
    std::cout << "进入副本构造器\n";
    *this = rhs;
    std::cout << "离开副本构造器\n";
}

MyClass::~MyClass()
{
    std::cout << "进入析构器\n";
    delete ptr;
    std::cout << "离开析构器\n";
}

MyClass &MyClass::operator=(const MyClass &rhs)
{
    std::cout << "进入赋值语句重载\n";
    if( this != &rhs )
    {
        delete ptr;

        ptr = new int;
        *ptr = *rhs.ptr;
    }
    else
    {
        std::cout << "赋值号两边为同个对象,不做处理!\n"; // obj1 = obj1;
    }

    std::cout << "离开赋值语句重载\n";

    return *this;
}

void MyClass::print()
{
    std::cout << *ptr << std::endl;
}

int main()
{
    MyClass obj1(new int(1));
    MyClass obj2(new int(2));
    obj2 = obj1;
    obj1.print();
    obj2.print();

    std::cout << "-------------------------------\n";

    MyClass obj3(new int(3));
    MyClass obj4 = obj3;
    obj3.print();
    obj4.print();

    std::cout << "-------------------------------\n";

    MyClass obj5(new int(5));
    obj5 = obj5;
    obj5.print();

    return 0;
}

     6.8.强制类型转换

       我们用传统的强制类型转换实现:把所需要的指针类型放在一对圆括号之间,然后写出将被强制转换的地址值。

Company *company = new Company("APPLE", "Iphone");
TechCompany *tecCompany = company;

       注意不能既删除company,又删除tecCompany。因为强制类型转换操作不会创建一个副本拷贝,它只是告诉编译器把有关变量解释为另一种类型组合形式,所以他们指向的是同一个地址。

       但它仍有一个问题没有解决:万一被强制转换的类型和目标类型结构完全不同,咋整? 编译器很笨的,它仍然将按照我们的代码行事!这样子的程序是相当危险的,随时可能崩溃以及被崩溃。 因为在类继承关系之间跳来转去(也就是对有关对象进行强制类型转换)在面向对象的程序里非常重要,所以C++程序员准备了几个新的强制类型转换操作符(动态对象强制类型转换):

       动态强制类型转换的语法更像是一个函数调用,先在两个尖括号之间写出想要的指针类型,然后将被转换的值写在括号中。

Company *company = new Company("APPLE", "Iphone");
TechCompany *tecCompany = dynamic_cast<TechCompany *>(company);
       if( tecCompany != NULL )
    {
        std::cout << "成功!\n";
    }
    else
    {
        std::cout << "失败!\n";
    }

    6.9.命名空间和模块化编程

       第一个概念是模块化(modularization):把程序划分成多个组成部分(即所谓的“模块”)这是通过把程序代码分散到多个文件里,等编译程序时再把那些文件重新组合在一起实现的。
       第二个概念是命名空间(namespace):这个概念相比起C语言是C++里新增加的东西,编写的程序越多、编写的程序越复杂,就越需要使用命名空间。

       C++预处理器的#include指令提供了一种能够让编译器在编译主程序时把其他文件的内容包括进来的机制。系统头文件的另一个重要作用是保证C++代码的可移植性,确保同样的C++代码在不同的操作系统上做同样的事情。例如为Mac定义的cout和为Windows定义的cout做的事情一样,但内部的具体实现不见得一样。在#include指令里,系统头文件的文件名要放在尖括号里给出,这是告诉编译器:应该到“标准的”地点寻找这个文件:#include <stdio.h>;自定义头文件的文件名要放在双引号里给出:#include “x.h”;

       绝大多数头文件是通用型的,不隶属于任何特定的程序,所以至少把它的用途和用法描述清楚。
       应该在注释里说明的内容包括:创建日期,文件用途,创建者姓名,最后一次修改日期,有什么限制和前提条件等等。
另外头文件里的每一个类和函数也应该有说明;典型的做法是只用它们来保存函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量。作为一个通用原则,应该把声明放在一个头文件里,把实现代码放在一个.cpp文件里。

       介绍下C预处理器:利用C++预处理器,我们可以让头文件只在这个类还没有被声明过的情况下才声明它。
       预处理器的条件指令:

       

       命名空间:随着程序变得越来越复杂,全局作用域里的东西会越来越多,尤其是在使用外部函数库时。这可能会演变成一个问题:因为没有两样东西可以有同样的名字。解决方案之一是给每个变量、函数和类等取一个独一无二的名字,但这可能很困难或很麻烦(因为随着一个程序代码量的逐步增加,一个变量名可能会变成像”CountOfItemsInTheArray”酱紫才能不重复)。

       命名空间其实就是由用户定义的范围,同一个命名空间里的东西只要在这个命名空间有独一无二的名字就行了。

因此,如果某个程序有许多不同的头文件或已编译文件,它们又各自声明了许多的东西,命名空间可以为它们提供保护。

       创建命名空间的办法很简单,先写出关键字namespace,再写出这个命名空间的名字,然后把这个命名空间里的东西全部括在一对花括号里就行了,如下所示:

namespace myNamespace
{
// 全部东西
}//末尾无分号
namespace author
{
    std::string person;
}
namespace programmer
{
    std::string person;
}

       一个小建议:不建议使用using namespace std这种形式,而是按需所取,例如using std::cout;

       如果你把它放在所有函数声明的前面,将拥有全局性,如果你把它放在某个函数里,那么它将只在这一个函数里可以使用。

//main.h
#include "rational.h"
#include <iostream>

int main()
{
    myMath::Rational f1(2, 16);
    myMath::Rational f2(7, 8);

    // 测试有理数加法运算
    std::cout << f1 << " + " << f2 << " == " << (f1+f2) << "\n";

    // 测试有理数减法运算
    std::cout << f1 << " - " << f2 << " == " << (f1-f2) << "\n";

    // 测试有理数乘法运算
    std::cout << f1 << " * " << f2 << " == " << (f1*f2) << "\n";

    // 测试有理数除法运算
    std::cout << f1 << " / " << f2 << " == " << (f1/f2) << "\n";

    return 0;
}


//rational.cpp
#include "rational.h"
#include <iostream>
#include <stdlib.h>

namespace myMath
{

Rational::Rational(int num, int denom)
{
    numerator = num;
    denominator = denom;

    normalize();
}

// normalize() 对分数进行简化操作包括:
// 1. 只允许分子为负数,如果分母为负数则把负数挪到分子部分,如 1/-2 == -1/2
// 2. 利用欧几里德算法(辗转求余原理)将分数进行简化:2/10 => 1/5
void Rational::normalize()
{
    // 确保分母为正
    if( denominator < 0 )
    {
        numerator = -numerator;
        denominator = -denominator;
    }

    // 欧几里德算法
    int a = abs(numerator);
    int b = abs(denominator);

    // 求出最大公约数
    while( b > 0 )
    {
        int t = a % b;
        a = b;
        b = t;
    }

    // 分子、分母分别除以最大公约数得到最简化分数
    numerator /= a;
    denominator /= a;
}

// a   c   a*d   c*b   a*d + c*b
// - + - = --- + --- = ---------
// b   d   b*d   b*d =    b*d
Rational Rational::operator+(Rational rhs)
{
    int a = numerator;
    int b = denominator;
    int c = rhs.numerator;
    int d = rhs.denominator;

    int e = a*b + c*d;
    int f = b*d;

    return Rational(e, f);
}

// a   c   a   -c
// - - - = - + --
// b   d   b   d
Rational Rational::operator-(Rational rhs)
{
    rhs.numerator = -rhs.numerator;

    return operator+(rhs);
}

// a   c   a*c
// - * - = ---
// b   d   b*d
Rational Rational::operator*(Rational rhs)
{
    int a = numerator;
    int b = denominator;
    int c = rhs.numerator;
    int d = rhs.denominator;

    int e = a*c;
    int f = b*d;

    return Rational(e, f);
}

// a   c   a   d
// - / - = - * -
// b   d   b   c
Rational Rational::operator/(Rational rhs)
{
    int t = rhs.numerator;
    rhs.numerator = rhs.denominator;
    rhs.denominator = t;

    return operator*(rhs);
}

std::ostream& operator<<(std::ostream& os, Rational f)
{
    os << f.numerator << "/" << f.denominator;
    return os;
}

}

// rational.h

// 这个头文件声明了有理数类(Rational class)
// 类里边对四则运算进行重载,以实现分数运算
#ifndef RATIONAL_H

#define RATIONAL_H

#include <iostream>

namespace myMath
{

class Rational
{
public:
    Rational(int num, int denom);  // num = 分子, denom = 分母

    Rational operator+(Rational rhs); // rhs == right hand side
    Rational operator-(Rational rhs);
    Rational operator*(Rational rhs);
    Rational operator/(Rational rhs);

private:
    void normalize(); // 负责对分数的简化处理

    int numerator;    // 分子
    int denominator;  // 分母

    friend std::ostream& operator<<(std::ostream& os, Rational f);
};

}

#endif

    6.10.链接与作用域

       主要是程序编译链接过程及auto,register,extern等关键字.

    6.11.模板(template)/泛型编程

       1:函数模板:比如我们存在swap(double &a, double &b)交换双精度,当整型/字符等腰重载函数来实现,这时候可使用函数模板;在泛型编程技术里,仍然要编写自己的函数和类,但不必限定它们所使用的数据类型。只需要使用一个占位符(通常用字母T来表示)然后用这个占位符来编写函数。当程序需要这段代码时,你提供数据类型,编译器将根据你的模板即时生成实用的代码。简单的说,编译器把模板里的每一个T替换为所提供的数据类型。注意不要把函数模板分成原型和实现两个部分。如果编译器看不到模板的完整代码,它就无法正确地生成代码。举个例子

#include <iostream>
#include <string>

template <class T>
void swap(T &a, T &b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int i1 = 100;
    int i2 = 200;

    std::cout << "交换前, i1 = " << i1 << ", i2 = " << i2 << "\n";
    swap(i1, i2);
    std::cout << "交换后, i1 = " << i1 << ", i2 = " << i2 << "\n";

    std::string s1 = "xyz";
    std::string s2 = "zyx";

    std::cout << "交换前, s1 = " << s1 << ", s2 = " << s2 << "\n";
    swap(s1, s2);
    std::cout << "交换后, s1 = " << s1 << ", s2 = " << s2 << "\n";

    return 0;
}

       2:类模板:语法,常用写法template <class T>

template <类型参数表>
class 类模板名{
    成员函数和成员变量
};

       template <class T>中的class并不意味着T只能是一个类。

template <class T>
class MyClass
{
    MyClass();
    void swap(T &a, T &b);
}
//构造器的实现将是下面这样:
MyClass<T>::MyClass()
{
// 初始化操作。
}

       编写一个基于模板的栈。

#include <iostream>
#include <string>

template <class T>
class Stack
{
public:
    Stack(unsigned int size = 100);
    ~Stack();
    void push(T value);
    T pop();

private:
    unsigned int size;
    unsigned int sp;
    T *data;
};

template <class T>
Stack<T>::Stack(unsigned int size)
{
    this -> size = size;
    data = new T[size];
    sp = 0;
}

template <class T>
Stack<T>::~Stack()
{
    delete []data;
}

template <class T>
void Stack<T>::push(T value)
{
    data[sp++] = value;
}

template <class T>
T Stack<T>::pop()
{
    return data[--sp];
}

int main()
{
    Stack<int> intStack(100);

    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";

    return 0;
}

       3:内联模板:内联即inline,第一想到的是内联函数,引入内联函数的目的是为了解决程序中函数调用的效率问题。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。

inline int add (int x, int y, int z)
{
return x+y+z;
}

       在程序中,调用其函数时,该函数在编译时被替代,而不像一般函数那样是在运行时被调用。在创建类模板时,避免类声明和类定义相分离的一个好办法是使用内联方法。在类里,内联方法的基本含义是在声明该方法的同时还对它进行定义。

class Person
{
  Person(std::string name)
  {
    this -> name = name;
  }
  // … …
}

       C++并没有限制只能使用一个类型占位符,如果类模板需要一种以上的类型,根据具体情况多使用几个占位符即可。

template <class T, class U>
class MyClass
{
// … …
}
//在实例化时,我们只需要这么做:
MyClass<int, float> myClass;
#include <iostream>
#include <string>

template <class T>
class Stack
{
public:
    Stack(unsigned int size = 100)
    {
        this -> size = size;
        data = new T[size];
        sp = 0;
    }

    ~Stack()
    {
        delete []data;
    }

    void push(T value)
    {
        data[sp++] = value;
    }

    T pop()
    {
        return data[--sp];
    }

private:
    unsigned int size;
    unsigned int sp;
    T *data;
};

int main()
{
    Stack<int> intStack(100);

    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";

    return 0;
}

第七章:错误处理和调试

       主要介绍:assert()函数&定义错误代码&try...catch等 

       可以在定义一个函数时明确地表明你想让它抛出一个异常,为了表明你想让它抛出哪种类型的异常,可以使用如下所示语法:

type functionName(arguments) throw(type);

       如果没有使用这种语法来定义函数,就意味着函数可以抛出任意类型的异常。注意如果catch中某一条语句执行失败抛出异常,则后续语句将不会再被执行,转而跳转执行throw中处理!

#include <iostream>
#include <climits>

unsigned long returnFactorial(unsigned short num) throw (const char *);

int main()
{
    unsigned short num = 0;

    std::cout << "请输入一个整数: ";
    while( !(std::cin>>num) || (num<1) )
    {
        std::cin.clear();             // 清除状态
        std::cin.ignore(100, '\n');   // 清除缓冲区
        std::cout << "请输入一个整数:";
    }
    std::cin.ignore(100, '\n');

    try
    {
        unsigned long factorial = returnFactorial(num);
        std::cout << num << "的阶乘值是: " << factorial;
    }
    catch(const char *e)
    {
        std::cout << e;
    }

    return 0;
}

unsigned long returnFactorial(unsigned short num) throw (const char *)
{
    unsigned long sum = 1;
    unsigned long max = ULONG_MAX;

    for( int i=1; i <= num; i++ )
    {
        sum *= i;
        max /= i;
    }

    if( max < 1 )
    {
        throw "该基数太大,无法在该计算机计算求出阶乘值。\n";
    }
    else
    {
        return sum;
    }
}

一些建议:

经验一:还是培养并保持一种良好的编程风格!

经验二:多用注释,用好注释。

经验三:注意操作符的优先级

经验四:千万不要忘记对用户输入和文件输入进行合法性检查

经验五:不要做任何假设

经验六:把程序划分成一些比较小的单元模块来测试

第八章:容器与算法

       略

猜你喜欢

转载自blog.csdn.net/chenjinnanone/article/details/109044769