静态对象强制类型转换
我们用传统的强制类型转换实现:把所需要的指针类型放在一对圆括号之间,然后写出将被强制转换的地址值。
Company *company = new Company(“APPLE”, “Iphone”);
TechCompany *tecCompany = company;
传统是这样的
TechCompany *tecCompany = (TechCompany *)company;
注意不能既删除company,又删除tecCompany。
因为强制类型转换操作不会创建一个副本拷贝,它只是告诉编译器把有关变量解释为另一种类型组合形式,所以他们指向的是同一个地址。现在术语称之为“重婚”!
万一被强制转换的类型和目标类型结构完全不同,咋整?
编译器很笨的,它仍然将按照我们的代码行事!这样子的程序是相当危险的,随时可能崩溃以及被崩溃。
因为在类继承关系之间跳来转去(也就是对有关对象进行强制类型转换)在面向对象的程序里非常重要,所以C++程序员准备了几个新的强制类型转换操作符(高级)!
只要你喜欢,你仍可以在C++里继续使用C的强制转换操作符(像刚才的栗子),但表中的操作符还能进行必要的类型检查,因而能够改善程序的可靠性。
动态强制类型转换的语法与刚刚我们学到的有很大不同,它看起来更像是一个函数调用:
Company *company = newTechCompany(“APPLE”, “Iphone”);
TechCompany *tecCompany = dynamic_cast<TechCompany *>(company);
先在两个尖括号之间写出想要的指针类型,然后是将被转换的值写在括号中。
内存泄漏
void foo()
{
My Class *x;
x = new MyClass();
}
当foo()函数结束时,指针变量x将超出它的作用域,这意味着它将不复存在,它的值当然就会丢失 new出来的内存在堆里,没有delete不会消除
有两种方法可以用来堵住这样的漏洞:
第一个方法是在return语句之前的某个地方插入一条delete x语句:
void foo()
{
MyClass*x;
x= new MyClass();
delete x;
x= NULL;
return;
}
第二个方法是让函数把内存块的地址返回给它的调用者:
MyClass*foo()
{
MyClass*x;
x= new MyClass();
return x;
}
这里需要特别注意的是,虽然动态分配的内存块没有作用域,但用来保存其地址的指针变量是受作用域影响的
命名空间和模块化编程
C++预处理器的#include指令提供了一种能够让编译器在编译主程序时把其他文件的内容包括进来的机制。
例如用这个指令来包括像iostream头文件我们已经用过很多次了。
头文件的基本用途是提供必要的函数声明和类声明。比如string头文件就定义了字符串应该如何创建和使用。
头文件可以细分为系统头文件和自定义头文件。
顾名思义,系统头文件定义的都是系统级功能,正式因为有了它们,C++代码才可以在某种特定的系统上运行。
如果你想在你的程序使用这些功能,就必须把相应的头文件包括到你的程序里来。
系统头文件的另一个重要作用是保证C++代码的可移植性,确保同样的C++代码在不同的操作系统上做同样的事情。
例如为Mac定义的cout和为Windows定义的cout做的事情一样,但内部的具体实现不见得一样。
在#include指令里,系统头文件的文件名要放在尖括号里给出,这是告诉编译器:应该到“标准的”地点寻找这个文件:
#include <stdio.h>
创建头文件
在#include指令里,自定义头文件的文件名要放在双引号里给出:#include “fishc.h”
头文件是一些以.h作为扩展名的标准文本文件。
一般情况下,都应该把自定义的头文件和其余的程序文件放在同一个子目录里,或者在主程序目录下专门创建一个子文件夹来集中存放它们。
你可以用头文件来保存程序的任何一段代码,如函数或类的声明,但一定不要用头文件来保存它们的实现!
与标准的C++源代码文件相比,在头文件里应该使用更多的注释。
绝大多数头文件是通用型的,不隶属于任何特定的程序,所以至少把它的用途和用法描述清楚。
应该在注释里说明的内容包括:
创建日期,文件用途,创建者姓名,最后一次修改日期,有什么限制和前提条件等等。
另外头文件里的每一个类和函数也应该有说明。
虽说头文件可以用来保存任意代码片段,但典型的做法是只用它们来保存函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量。
如果你有一个程序需要多次调用一个或一组函数,或是你有一个或一组函数需要在多个程序里调用,就应该把它们的声明拿出来放到一个头文件里。
头文件应该只包含最必要的代码,比如只声明一个类或只包含一组彼此相关的函数。
使用头文件
在创建了头文件之后,只要把它的文件名用双引号括起来写在如下所示的指令里就可以导入它:
#include “fishc.h”
如果没有给出路径名,编译器将到当前子目录以及当前开发环境中的其他逻辑子目录里去寻找头文件。
为了消除这种猜测,在导入自己的头文件时可以使用相对路径。如果头文件与主程序文件在同一个子目录里,则可以这么写:
#include “./fishc.h”
如果头文件位于某个下级子目录里,那么以下级子目录的名字开头:
#include “includes/fishc.h”
最后,如果头文件位于某个与当前子目录平行的”兄弟”子目录里,则需要这么写:
#include “../includes/fishc.h”
请务必注意,Windows通常使用反斜杠作为路径名里的分隔符。
这样的。。 .\\ 注意点双反斜杠
C预处理器
刚才的示例程序还有一个小小的问题需要解决,就是rationcal.cpp和main.cpp文件都包含了rational.h头文件。
这意味着rational.h类被声明了两次,这显然没有必要(如果它是一个结构,声明两次还将导致编译器报错呢~)
解决方案之一是把其中一个文件里的#include删掉即可。这固然很容易可以解决的问题,但却会给今后留下麻烦。。。。。。
当然我们在这里提出是因为有更好的解决方案!
利用C++预处理器,我们可以让头文件只在这个类还没有被声明过的情况下才声明它。
以前的课程中,我们曾建议大家注释很多段代码的话用预处理的方式,比起/* */要效果好:
#if 0
// 这里有代码
// 这里有好多代码
// 这里有好多好多代码
// 这里有好多好多好多代码
#endif
#ifndef LOVE_FISHC
#define LOVE_FISHC
#endif
这看起来好像没什么用,但事实却并非如此。这段代码的含义是:如果LOVE_FISHC还没有定义则定义之,看出这有什么作用了吗?
#ifndef LOVE_FISHC
#define LOVE_FISHC
class Rational{ … };
#endif
如果LOVE_FISHC还没有定义,这里将发生两件事儿:定义一次LOVE_FISHC,然后对Rational类做出声明等操作。
这样一来,即使包含着这段代码的文件在某个项目里被导入了100次,Rational类也只会被声明一次,因为在第一次之后LOVE_FISHC就有定义!
作为一种固定模式,这里使用的常量名通常与相应的文件名保持一致,把句点替换为下划线。
于是,rational.h文件将对应RATIONAL_H
创建命名空间
创建命名空间的办法很简单,先写出关键字namespace,再写出这个命名空间的名字,然后把这个命名空间里的东西全部括在一对花括号里就行了,如下所示:
namespace myNamespace
{
//全部东西
}
注意在最末尾不需要加上分号哦。
正如我们刚才讲过的那样,命名空间可以让你使用同一个标识符而不会导致冲突:
namespace author
{
std::string person;
}
namespace programmer
{
std::string person;
}
现在,我们来把Rational类的定义放到它自己的命名空间里去。
这样一来,我们就再也用不着担心它会与也叫做Rational的其他东西发生冲突了。
想要访问在某个命名空间里定义的东西,有三种方法可供选择
第一种方法我们已经用了很多遍了:
std::cout << “I love fishc.com!n”;
第二种方法是使用using指令:
using namespace std;
执行这条语句后,在std命名空间里定义的所有东西就都可以使用,我们便可以像下面直接使用:
cout << “I love fishc.com”
不过,把命名空间里的东西带到全局作用域里,跟我们使用命名空间的本意相违背!
所以,小甲鱼不建议在文件开头直接用using namespace XX这种设计风格。
最后一种方法是用一个using指令只把你需要的特定命名从命名空间提取到全局作用域:
using std::cout;
cout << “I love fishc.com!n”;
最后请务必注意:using指令的出现位置决定着从命名空间里提取出来的东西能在哪个作用域内使用。
如果你把它放在所有函数声明的前面,他将拥有全局性,如果你把它放在某个函数里,那么它将只在这一个函数里可以使用。
链接和作用域
存储类(storage class)
每个变量都有一个存储类,它决定着程序将把变量的值存储在计算机上的神马地方、如何存储,以及变量应该有着怎样的作用域。
默认的存储类是auto(自动),但你不会经常看到这个关键字,因为它是默认的,阴魂不散的!
自动变量存储在称为栈(stack)的临时内存里并有着最小的作用域,当程序执行到语句块或函数末尾的右花括号时,它们将被系统回收(栈回收),不复存在。
与auto不同的是static,static变量在程序的生命期内将一直保有它的值而不会消亡,因为它们是存储在静态存储区,生命周期为从申请到程序退出(和全局变量一样)。
另外我们稍后就会提到的,一个static变量可以有external或internal链接。
第三种存储类是extern,它在有多个翻译单元时非常重要。这个关键字用来把另一个翻译单元里的某个变量声明为本翻译单元里的一个同名全局变量。
注意,编译器不会为extern变量分配内存,因为在其他地方已经为它分配过内存。
用extern关键字相当于告诉编译器:“请相信我,我发誓我知道这个变量在其他翻译单元里肯定存在,它只是没在这个文件里声明而已!”
还有一个存储类是register,它要求编译器把一个变量存储在CPU的寄存器里。但有着与自动变量相同的作用域。
register变量存储速度最快,但有些编译器可能不允许使用这类变量。
变量的链接和作用域
链接是一个比较深奥的概念,所以我们尽可能以浅显的文字来解释它。
在使用编译器建议程序时,它实际上是由3个步骤构成:
- 执行预处理器指令;
- 把.cpp文件编译成.o文件;
- 把.o文件链接成一个可执行文件。
如今的编译器都是一次完成所有的处理,所以你看不到各个步骤(学习Win32汇编的童鞋懂~)
步骤一前边我们已经讨论过:执行预处理指令,例如把#include指令替换为相应的头文件里的代码,总的效果是头文件里的代码就像从一开始就在.cpp文件里似的。
步骤二是我们司空见惯的事情:把C++代码转换为一个编译目标文件,在这一步骤里,编译器将为文件里的变量分配必要的内存并进行各种错误检查。
如果只有一个C++源文件,步骤三通常只是增加一些标准库代码和生成一个可执行文件。
但当你同时编译多个源文件来生成一个可执行文件的时候,在编译好每一个组件之后,编译器还需要把它们链接在一起才能生成最终的可执行文件。
当一个编译好的对象(即翻译单元)引用一个可能不存在于另一个翻译单元里的东西时,潜在的混乱就开始出现了。
链接分为三种情况,凡是有名字的东西(函数、类、常量、变量、模板、命名空间,等等)必然属于其中之一:外连接(external),内链接(internal)和无链接(none)。
外链接的意思是每个翻译单元都可以访问这个东西(前提是只要它知道有这么个东西存在)。
普通的函数、变量、模板和命名空间都有外链接。
就像main.cpp可以使用rational.cpp文件里定义的类和函数一样,其实我们一直在使用,只是今天我们来一次总结。
说到变量,你可以这样试一试:
// this.cpp
int i1=1;
// that.cpp
int i2= i1;
不用试了,一看就有问题,对不对?!在编译that.cpp文件时,编译器并不知道i1变量的存在。
为了解决这个问题,我们可以在that.cpp里使用extern关键字去访问第一个翻译单元的变量。
// this.cpp
int i1=1;
// that.cpp
externint i1;
int i2= i1;
内链接的含义是:在某个翻译单元里定义的东西只能在翻译单元里使用,在任何函数以外定义的静态变量都有内链接:
// this.cpp
staticint d=8;
// that.cpp
staticint d=9;
这两个文件各有一个同名的变量,但它们是毫不相干的两样东西。
最后,在函数里定义的变量只存在于该函数的内部,根本没有任何链接(none)。
注意:
不要在头文件中定义变量。也就是
#ifndef HEADER_H
#define HEADER_H
unsigned short headerNum = 5;
#endif
虽然用了if end。。但会出现这个变量重复定义。。所以最好是只声明
如果在头文件中非要初始化变量要声明为 static const unsigned short headerNum = 5; 。。。。。固定住
extern unsigned short thatNum; extern能把别的文件中定义的变量拉到本文件中用,注意不是在头文件中的。。
例
header.h
#ifndef HEADER_H
#define HEADER_H
unsigned long returnFactorial(unsigned short num);
static const unsigned short headerNum = 5;
/*
this.obj : error LNK2005: "unsigned short headerNum" (?headerNum@@3GA) already defined in that.obj
链接时出现这种错误说明headerNum被重复定义
虽然用了if end。。但会出现这个变量重复定义。。所以最好是只声明
如果在头文件中非要初始化变量要声明为 static const unsigned short headerNum = 5; 。。。。。固定住
*/
#endif
this.cpp
#include "header.h"
#include <iostream>
extern unsigned short thatNum;//extern能把别的文件中定义的变量拉到本文件中用,注意不是在头文件中的。。
static bool printMe = false;//static内联接 是静态变量,存放在本文件中,不会改变
int main()
{
unsigned short thisNum = 10;
std::cout << thisNum << "! is equal to " << returnFactorial(thisNum) << "\n\n";
std::cout << thatNum << "! is equal to " << returnFactorial(thatNum) << "\n\n";
std::cout << headerNum << "! is equal to " << returnFactorial(headerNum) << "\n\n";
if (printMe)//这里没打印下面这句所以printMe为false
{
std::cout << "小甲鱼真帅!\n\n";
}
return 0;
}
that.cpp
#include "header.h"
unsigned short thatNum = 8;
bool printMe = true;
unsigned long returnFactorial(unsigned short num)
{
unsigned long sum = 1;
for ( int i=1; i<=num; i++)
{
sum *= i;
}
if ( printMe )//这地方正确返回sum的值所以printMe为true
{
return sum;
}
else
{
return 0;
}
}
函数模板
接下来的这几讲里,我们先来学习如何编写和使用自己的泛型代码,然后再跟大家介绍标准模板库(Standard Template Library, STL)。
在泛型编程技术里,我么仍然需要编写自己的函数和类,但不必限定它们所使用的数据类型。
只需要使用一个占位符(通常用字母T来表示)然后用这个占位符来编写函数。
当程序需要这段代码时,你提供数据类型,编译器将根据你的模板即时生成实用的代码。
以下代码定义了一个名为foo()的函数模板:
void foo(T param)
{
// do something
}
这里有几件事值得注意:
1. 第一行代码里,在尖括号里有一个class T,用来告诉编译器:字母T将在接下来的函数里代表一种不确定的数据类型。
2. 关键字class并不意味着这个是类哦,这只是一种约定俗成的写法。
3. 在告诉计算机T是一种类型之后,就可以像对待一种普通数据类型那样使用它了。
例
#include <iostream>
#include <string>
template <class T>
void swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
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 = "小甲鱼";
std::string s2 = "小尤鱼";
std::cout << "交换前,s1 = " << s1 << ", s2 = " << s2 << "\n";
swap(s1, s2);
std::cout << "交换后,s1 = " << s1 << ", s2 = " << s2 << "\n";
return 0;
}
需要注意的地方
在创建模板时,还可以用template <typename T>来代替template <class T>,它们的含义是一样一样的。
注意,template <class T>中的class并不意味着T只能是一个类。
再强调一次,不要把函数模板分成原型和实现两个部分。
如果编译器看不到模板的完整代码,它就无法正确地生成代码。
所得到的出错信息从“不知所云”到“胡说八道”什么样都有。
为了明确地表明swap()是一个函数模板,还可以使用swap<int>(i1, i2)语法来调用这个函数。
这将明确地告诉编译器它应该使用哪一种类型。
如果某个函数对所有数据类型都将进行同样的处理,就应该把它编写为一个模板。
如果某个函数对不同的数据类型将进行不同的处理,就应该对它进行重载。
类模板
类模板与函数模板非常相似:同样是先由你编写一个类的模板,再由编译器在你第一次使用这个模板时生成实际代码。
template<class T>
class MyClass
{
MyClass();
void swap(T&a, T&b);
}
构造器的实现将是下面这样:
MyClass<T>::MyClass()
{
//初始化操作。
}
因为MyClass是一个类模板,所以不能只写出MyClass::MyClass(),编译器需要你在这里给出一种与MyClass()配合使用的数据类型,必须在尖括号里提供它。
因为没有确定的数据类型可以提供,所以使用一个T作为占位符即可。
我们即将编写一个基于模板的栈。
栈是实际编程过程中一种非常有用的数据结构,它是一种数据存储机制。
栈只提供两个函数:一个用来吧数据压入栈的顶部,另一个用来从栈取出顶部元素(先进后出)
例
#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;
}
内联模板
内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。
一般在代码中用inline修饰,但能否形成内联函数,需要看编译器对该函数定义的具体处理。
举个栗子:
inlineint add(int x,int y, int z)
{
return x+y+z;
}
在程序中,调用其函数时,该函数在编译时被替代,而不像一般函数那样是在运行时被调用。
这东西可以在定义类模板时,函数的定义不用写到外面,内联到类的内部定义
容器和算法
向量
向量容器
数组这种数据结构最大的先天不足就是它受限于一个固定的长度。
在程序里用int myArray[40]这样的语句定义一个数组时,程序将遇到两个问题:
首先,你最多只能在那个变量里存储40个整型数据,万一你需要存储第41个数据,那么你就相当不走运了。
其次,不管程序是不是真的需要存储40个整型数据,编译器都会为它分配40个整型数据的空间。
像这样的问题用C语言解决起来往往很复杂,而C++提供的解决方案就高明得多了。
C++标准库提供的向量(vector)类型从根本上解决了数组先天不足的问题。
就像可以创建各种不同类型的数组一样,我们也可以创建各种不同类型的向量。
std::vector<type> vectorName;
这种语法相信鱼油们应该不会再感到陌生了。
我们用不着对一个向量能容纳多少个元素做出限定,因为向量可以动态地随着你往它里面添加元素而无线增大(前提是有足够可用的内存)
然后你还可以用它的size()方法查知某给定响亮的当前长度(它当前包含的元素个数)
定义一个向量后,我们可以用push_back()方法往它里边添加东西。
我们还可以用访问数组元素的语法来访问某给定向量里的各个元素。
#include <iostream>
#include <string>
#pragma warning(disable:4786)//禁用4786警告必须在引用vector头文件之前
#include <vector>
/*
在#include<iostream>前面添加
#pragma warning(disable:4786);
这个是VC的BUG;
<由于stl里用的字符串过长>,VC搞不定了.. 直接禁止即可.
*/
int main()
{
std::vector<std::string> names;
names.push_back("小甲鱼");
names.push_back("小尤鱼");
for ( int i=0; i<names.size(); i++ )
{
std::cout << names[i] << "\n";
}
return 0;
}
迭代器
例
#include <iostream>
#include <string>
#pragma warning(disable:4786)//禁用4786警告必须在引用vector头文件之前
#include <vector>
/*
在#include<iostream>前面添加
#pragma warning(disable:4786);
这个是VC的BUG;
<由于stl里用的字符串过长>,VC搞不定了.. 直接禁止即可.
*/
int main()
{
std::vector<std::string> names;
names.push_back("小甲鱼");
names.push_back("小尤鱼");
std::vector<std::string>::iterator iter = names.begin();//begin是指向起始位
while ( iter != names.end() )//end指向最后一个数据下一个内容
{
std::cout << *iter << "\n";
++iter;
}
return 0;
}
算法
例
#include <iostream>
#include <string>
#pragma warning(disable:4786)//禁用4786警告必须在引用vector头文件之前
#include <vector>
#include <algorithm>
/*
在#include<iostream>前面添加
#pragma warning(disable:4786);
这个是VC的BUG;
<由于stl里用的字符串过长>,VC搞不定了.. 直接禁止即可.
*/
int main()
{
std::vector<std::string> names;
names.push_back("Larry");
names.push_back("Rola");
names.push_back("DingDing");
names.push_back("Joyjoy");
names.push_back("Michael");
names.push_back("Lucy");
names.push_back("Lilei");
std::sort(names.begin(), names.end());
std::vector<std::string>::iterator iter = names.begin();//begin是指向起始位
while ( iter != names.end() )//end指向最后一个数据下一个内容
{
std::cout << *iter << "\n";
++iter;
}
return 0;
}