C++ 教程
[toc]
符号
using namespace std;
这条指令是一个全新的概念:名字空间
所有标识符都在一个特殊的名字空间
std
中来定义,用以区分不同的命名版本cout <<
有此指令std::cout <<
无此指令
让程序访问名称空间
std
的方法- 将指令放在函数定义前,让文件中所有函数都能使用名称空间std中所有元素
- 将指令放在特定的函数定义中,让该函数能够使用名称空间std中所有元素
- 在特定的函数中使用类似
using std::cout;
这样编译指令,让该函数使用指定的元素,如cout
- 完全不使用编译指令
using
,而在需要使用名称空间std中的元素时,使用前缀std::
cout
- 全名
console out
,cout
是一个输出流对象
«
- 名称:插入运算符
- 在 c 中是左移操作符
- 在 c++ 中它有另一个特点:支持重载。
- 重载,即同一个运算符将有不同的含义。编译器通过上下文来确定运算符的含义。
cin
- 流对象,从用户终端读取数据
»
- 输入操作符又称提取符,它一次从输入流对象
cin
提取一个元素 - 如果用户不进行键盘输入,则程序会阻塞。
const 限定符
作用:只要一个变量前面用
const
来修饰,该变量里的数据可以被访问,不能被修改,也就是只读。const
与 宏定义(define
)效果差不多只要一个变量前面用
const
来修饰,该变量里的数据可以被访问,不能被修改,也就是只读。const
与 宏定义(define
)效果差不多规则:
const
离谁近,谁就不能被修改;比较复杂时,从右往左读const
修饰一个变量,一定要给这个变量初始化值,若不初始化,后面就无法初始化。const type name = value;
//定义成 const
后的常量,程序对其中只能读不能修改。
//以下程序是错误的,因为开头就已经固定了常量,便不能再对其进行赋值:
#include <iostream>
using namespace std;
int main()
{
const double pi; //圆周率的值用pi表示
pi=3.14159265;
cout<<"圆周率的近似值是"<<pi<<endl;
return 0;
}
//下面给出正确的赋值方法:
#include <iostream>
using namespace std;
int main()
{
const double pi=3.141592; //圆周率的值用pi表示
cout<<"圆周率的近似值是"<<pi<<endl;
return 0;
}
与
define
相比- 可以明确指定类型
- 可以使用作用域规则将定义限制在特定的函数或文件中
- 可以将
const
用于更复杂的类型
endl
- 名称:控制符。作用:重起一行。光标将被移到下一行开头。
\n
也可以用,不过,使用它不能保证程序继续运行前将其立即显示在屏幕上
=
- 名称:赋值运算符,在 c 和 c++ 中可以连续使用,如
a=b=c=1
大括号初始化器,使用它初始化时,可以使用等号(=),也可以不使用
int emus{7};
int emus={7};
大括号中可以不包含任何东西,变量将被初始化为0;
int rocs={};
有助于更好地防范类型转换错误
::
在 C++ 中表示作用域,和所属关系。
:: 是运算符中等级最高的,它分为三种,分别如下:
作用域符号 :: 的前面一般是类名称,后面一般是该类的成员名称,C++ 为例避免不同的类有名称相同的成员而采用作用域的方式进行区分。
例如:A,B 表示两个类,在 A,B 中都有成员 member
那么:
1、
A::member
就表示类 A 中的成员 member2、
B::member
就表示类 B 中的成员 member
- 全局作用域符号:当全局变量在局部函数中与其中某个变量重名,那么就可以用 :: 来区分
例如:
char zhou; //全局变量 void sleep() { char zhou; //全局变量 char(局部变量) = char(局部变量)*char(局部变量); ::char(全局变量) =::(全局变量) *char(全局变量) }
- 作用域分解运算符,比如声明了一个类 A,类 A 里声明了一个成员函数 void f(),但没有在类的声明里给出f的定义,那么在类外定义 f 时,就要写成 voidA::f(),表示这个 f() 函数是类 A 的成员函数。
例如:
class CA { public: int ca_var; int add(int a, int b); int add(int a); } //那么在实现这个函数时,必须这样写: int CA::add(int a, int b) { return a + b; } //另外,双冒号也常常用于在类变量内部作为当前类实例的元素进行表示,比如: int CA::add(int a) { return a + ::ca_var; } //表示当前类实例中的变量ca_var。
:
1、类构造函数 (Constructor) 的初始化列表
在构造函数后面紧跟着冒号加初始化列表,各初始化变量之间以逗号 (,) 隔开。下面举个例子。
class myClass { public : myClass();// 构造函数,无返回类型,可以有参数列表,这里省去 ~myClass();// 析构函数 int a; const int b; } myClass::myClass():a(1),b(1)// 初始化列表 { }
上面的例子展示了冒号的这个用法,下面对这个用法进行几点说明:
1)初始化列表的作用相当于在构造函数内进行相应成员变量的赋值,但两者是有差别的。
在初始化列表中是对变量进行初始化,而在构造函数内是进行赋值操作。两都的差别在对于像const类型数据的操作上表现得尤为明显。我们知道,const类型的变量必须在定义时进行初始化,而不能对const型的变量进行赋值,因此const类型的成员变量只能(而且必须)在初始化列表中进行初始化,即下面的代码将会出错:
myClass::myClass() { a = 1;// 没错,效果相当于在初始化列表中进行初始化 b = 1;// 出错,const变量不能进行赋值操作; }
2)初始化的顺序与成员变量声名的顺序相同。
先看一下下面的程序:
myClass::myClass():b(1),a(b) { }
这样的执行结果a,b各是多少呢?b=1,a=1?不是,b=1而a是个随机数。这一点是相当重要的哦,一般在初始化列表中进行初始化时,初始化的顺序应与声明的顺序保持一致,防止出现不必要的错误。
3)对于继承的类来说,在初始化列表中也可以进行基类的初始化,初始化的顺序是先基类初始化,然后再根据该类自己的变量的声明顺序进行初始化。
2、声明基类。
假设我们重新定义一个类,继承自myClass类。定义方式如下:
class derivedClass : public myClass { // 略去 }
这里的冒号起到的就是声名基类的作用,在基类类名前面可以加 public\private\protected 等标签,用于标识继承的类型,也可以省略,省略的话,用 class 定义的类默认为 private ,用 struct 定义的类默认为 public ,至于具体各个标签有什么区别这里就不说了。
与初始化列表一样的,这里也可以声名多个基类,各基类之间用逗号(,)隔开。
其它字符
- \n 换行符
- \t 水平制表符
- \v 垂直制表符
- \b 退格
- \r 回车
- \a 振铃
- \ 反斜杠 \
C++基本语法
函数
cin
cin.peek();
就是返回输入流里面的第一个字符,但是不会像get
那样取出来cin.get(数组名,长度,结束符);
会提取出输入的第一个字符cin.ignore(长度,结束符);
从输入流(cin)中提取字符,提取的字符被忽略(ignore),不被使用的cin.getline(数组名,长度,结束符);
提取一行cin.read(buf,20);
把数据读入数据流中cin.clear();
清理错误表示符
cout
cout.precision();
精度cout.width();
长度
cint(小数);
将此小数四舍五入 其它方法:int x=2.6; int i=(int)(x+0.5);
文件
in
,out
getc()
函数一次从输入流(stdin)读取一个字符,返回值是int类型。putc()
函数把这个字符写入到输出流(stdout)- EOF 宏定义
end of file
一般是文件的结尾,值为-1
open()
指针
概念:地址是计算机内存中的某个位置,指针是专门用来存放地址的特殊类型变量
形式:
type *pointerName;
- 允许void类型指针
内存:程序在硬盘上以文件的形式存在,但它们的运行在计算机的内存中发生的
对齐:变量类型是根据它们的自然边界进行对齐的。不同操作系统对齐字节不同
- 文件对齐,内存对齐
- 程序在编译链接后会被分割成一个一个的区块,而区块在文件和内存中要按照一定的规律来对齐
- 文件对齐,内存对齐
寻址
通过变量名
通过变量地址
- 变量的地址在程序执行期间是不会发生变化的
- 不过,同一个程序不同时间加载到内存中,同一个变量的地址是会改变的
’&‘ 取址操作符,给变量取别名
int var = 123; std::cout <<"Address is :" <<&var;
可以把地址赋值给一种称为指针的特殊变量
指针类型必与由它保存其地址的变量的类型一致
’*‘ 解引用符
c++ 允许指针群 p ,就是多个指针有同样的值
int*p1=&myInt; int*p2=&myInt;
c++支持无类型(void)指针,就是没有被声明为某种特定类型的指针
void*vPointer;
reinterpret_cast<type> (expr): reinterpret_cast
运算符把某种指针改为其他类型的指针。它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。数组的名字同时也是一个指向其第一个元素(基地址)的指针。
传值,传址和传引用
在默认情况下,参数只能以值传递的方式给函数
- 被传递到函数的只是 { 变量的值 },永远不会是变量本身
如何绕开“传值”?
传地址
- 向函数 { 传递变量的地址 } 取代它的值
- 想要 { 获取某个变量的地址 } 只需要在它前面加“取址符”【&】
- 注意:如果传的是地址,在函数中必须要通过【*】对指针进行解引用
引用传递
- 声明时:
swap(int &x,int &y);
- 用函数时:
swap(num1,num2);
- 声明时:
反汇编
结构
定义结构的语法
struct name { type varName1; type varName2; };
用 “ . " 对结构成员进行赋值
结构与指针
例子
struct FishOil { std::string name; std::string id; char sex; //F==Female,M=Male }; //创建一个 FishOil 类型的变量 FishOil Jiayu={"小甲鱼”,"fishc_00000",'M'} //创建一个指向该结构的指针 FishOil *pJiayu=&Jiayu; /*注意:因为指针的类型必须与指向的地址的变量的类型一致,所以pJiayu指针的类型也是FishOil */
通过指针访问结构成员
对指针进行解引用来访问相应的变量值
(*pJiayu).name="黑夜";
(*pJiayu).id="fishc_00001";
用箭头
pJiayu->name="黑夜";
pJiayu->id="fishc_00001";
区分".“与”->"
- 把(*pJiayu)当作结构变量时用"."
- 把 pJiayu 当作指针时用"->"
联合,枚举和类型别名
联合(union)
union mima { unsigned long birthday; unsigned short ssn; char* pet; }; //创建该类型的变量 mima mima_1; //赋值 mima_1.birthday=20010101; 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* intPointer;
- Typedef,为一个类型定义别名
函数指针和指针函数
函数指针:指向函数首地址的指针变量称为函数指针
- 声明
int (*p)( );
- 声明
指针函数:一个函数可以带回一个整型数据的值,字符类型值和实型类型的值,还可以带回指针类型的数据,使其指向某个地址单元
面对对象
类和对象
类和对象基础
类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
操作文件的对象
ifstream,(input file stream)
ifstream in;
in.open("test.txt");
等价于
ifstream in("test.txt");
默认操作为打开两个参数
ifstream in(char* filename,int open_mode);
filename 文件名称,它是一个字符串
open_mode 打开模式,其值用来定义以怎样的方式打开文件
常见的打开模式
- ios::in–打开一个可读取文件
- ios::out–打开一个可写入文件
- ios::binary–以二进制的形式打开一个文件
- ios::app–写入的所有数据将被追加到文件的末尾
- ios::trunk–删除文件原来已存在的内容
- ios::nocreate–如果要打开的文件并不存在,那么以此参数调用open函数将无法进行
- ios::noreplace–如果要打开的文件已存在,试图用open函数打开时将返回一个错误
- ios::beg–使得文件指针指向文件头
- ios::end–使得文件指针指向文件尾
并行操作OR符号 “ | ”
ofstream,(output file stream)
ofstream in;
out.open("text.txt");
等价于
ofstream in("test.txt");
区分类和结构
- 对象内部可以有变量和函数
- 结构通常只由各种变量构成
声明一个类
class Car { public: std::string color; std::string engine; float gas_tank; unsigned int Wheel; void fill_tank(float liter); //方法的声明 }; //方法的定义通常安排在类声明的后面 void Car::fill_tank(float liter) { gas_tank +=liter; }
注意:类名的第一个字母大写是一种习惯上的标准,但不是硬性规定,在类声明末尾必须有一个分号【;】
类由变量和函数组成,对象将使用那些变量来存储信息,调用那些函数来完成操作
- 类里的变量成为属性,函数成为方法
作用域解析操作符【::】,作用是告诉编译器这个方法存在于何处,或者说属于哪一个类
定义构造器——类的构造函数
面向对象的编程技术开发程序最基本步骤
- 定义一个由属性和方法的类(模板)
- 为该类创建一个变量(实现)
区别
- 构造器的名字必须和它所在的类的名字一样
- 系统在创建某个类的实例时会第一时间自动调用这个类的构造器
- 构造器永远不会返回任何值,并且构造函数没有声明类型
创建构造器,需要先把它的声明添加到类里:
class Car{ Car(void); } //注意大小写与类名保持一致。在结束声明之后开始定义构造器本身 Car::Car(void) //不用写void Car::Car(void) { color = "WHITE"; engine = "V8"; wheel = 4; gas_tank = FULL_GAS; }
构造对象数组:数组可以是任何一种数据类型
Car mycar[10]; //调用语法 mycar[x].running; //注:x代表着给定数组元素的下标
每个类至少有一个构造器,如果你没有在类里定义一个构造器,编译器就会替你定义一个没有代码内容的空构造器:
ClassName::ClassName(){}
除此之外编译器还会替你创建一个副本构造器。
定义析构器——类的析构函数
析构器:在销毁一个对象时,系统会调用析构器来达到效果
class Car { Car(void); ~Car(); }
构造器用来完成事先的初始化和准备工作(申请分配内存),析构器用来完成事后所需的清理工作(清理内存)
特点
- 析构器也永远不返回任何值
- 析构器不带任何参数,格式:
~ClassName();
在复杂的类里,析构器往往至关重要(可能引起内存泄漏)
副本构造器
可以把一个对象赋值给一个类型与之相同的变量
- 编译器将生成必要的代码把“源”对象各属性的值分别赋值给“目标”对象的对应成员。这种赋值行为叫逐位复制
问题:源对象的成员变量是指针,对象成员进行逐位复制的结果是你将拥有两个一摸一样的实例,而这两个副本里的同名指针会指向相同的地址。当删除其中一个对象时,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,就会出现问题!
//例1 MyClass obj1; MyClass obj2; obj2=obj1;
解决思路:重载操作符
重载“=“操作符,在其中对指针进行处理
- 语法:
MyClass &operator = (const Myclass &rhs);
//这个方法预期的输入参数是一个MyClass类型的、不可改变的引用 - 因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为它创建另外一个副本(否则可能导致无限递归)
- 返回一个引用,该引用指向一个MyClass类的对象,这样做的好处时方便我们把一组赋值语句串联起来,如a=b=c;
- 语法:
例2 MyClass obj1; MyClass obj2=obj1;
- 编译器将再MyClass类里寻找一个副本构造器(copy constructor),如果找不到,它会自行创建一个
- 这样,即使已经在这个类里重载了赋值操作符暗藏着隐患的”逐位复制“行为还是会发生
想要躲开这个隐患,还需要亲自定义一个副本构造器,而不是让系统帮我们生成
MyClass(const MyClass &rhs);
- 这个构造器需要一个固定不变(const)的MyClass类型的引用作为输入参数,就像赋值操作符那样。因为他是一个构造器,所以不需要返回类型
访问控制
public:语句声明
访问控制是c++提供的一种【保护】类里的方法和属性的手段
【保护】指对谁可以调用某个方法和访问某个属性加上一个限制。如果某个对象试图调用一个它无权访问的函数,编译器将报错。
C++中的访问级别
- 【public】任何代码
- 【protected】这个类本身和它的子类
- 【private】只有这个类本身
注:BUG无法避免的原因是因为我们无法模拟各种情况的输入和修改带来的影响
使用private的好处是,之后可以只修改某个类的内部实现,而不必重新修改整个程序。这是因为其它代码根本访问不到private保护的内容
在同一个类定义里可以使用多个public:,private:和protected:语句,但最好集中在一个地方,提高可读性
在编写类定义代码时,应该从public:开始写起,然后是protected,最后是private
友元关系
- 需要访问到某个protected成员,甚至某个private,该怎么实现?
- 友元关系允许友元访问对方的public方法和属性,还允许友元访问对方的protected和private方法和属性
- 声明语法,只要在类声明里加上一条【friend class**】{【**】表示需要友元的名字 }
- 注:这条语句可以放在任何地方,放在public,protected,private段落里都可以
内联函数
引入内联函数目的:解决程序中函数调用的效率问题
从源代码层来看,内联函数有函数结构,而在编译却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名
一般在代码中用inline修饰,但能否形成内联函数,需要看编译器对该函数定义具体处理
inline int add(int x,int y,int z) { return x+y+z; }
在程序中,调用其函数时,该函数在编译时被替代,而不像一般函数那样是在运行时被调用
类模板和函数模板的创建过程几乎没什么区别
- 把相关代码放在一起,这条规则同样适用于类模板
- 不管是什么模板,编译器都必须看到全部的代码才能为一种给定的类型创建出一个新的实现来
- 在创建类模板时,避免类声明和类定义相分离的一个好办法是使用内联方法
- 在类里,内联方法的基本含义是在声明该方法的同时还对它进行定义
- 语法
使用内联模板的好处:让程序员少打字并让源代码的可读性变得更好
- 使用Stack模板前,一定要给它添加一个副本构造器和一个赋值操作符重载
- 因为代码缺少错误处理功能,例如在栈满时调用 push() 方法,或者在栈为空的时候调用 pop() 方法,会导致程序运行出错
在C++里可以使用多个类型占位符,如果类模板需要一种以上的类型,根据具体情况多使用几个占位符即可
this指针
this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在对象的地址
例子
class Human{ char fishc; Human(char fishc); }; Human::Human(char fishc){ fishc = fishc; };
this指向当前类的属性
改为
this->fishc = fishc;
//左边为当前对象的fishc属性,右边为构造器的传入来的fishc参数注意:使用this指针的基本原则,如果代码不存在二义性,就不用this指针
静态属性和静态方法
面对对象编程技术的一个重要特征是用一个对象把数据和对数据处理的方法封装在一起
如果我们所需的功能或数据不属于某个特征的对象,而是属于整个类的,该怎么办?
c++允许我们把一个或多个成员声明为属于某个类,而不是仅属于该类的对象。
好处
- 程序员可以在没有创建任何对象的情况下调用有关的方法
- 能够让有关的数据仍在该类的所有对象间共享
创建一个静态属性和静态方法:
- 只需要在它的声明前加上static保留字即可
static
- 隐藏:static作为函数的前缀时,可以对其它源文件隐藏该函数
- 保持变量内容持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也就是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,与全局变量比起来,static可以控制变量的可见范围,说起来还是隐藏
- 默认初始化为0,全局变量和static定义的变量都有这个作用
静态方法与this指针的关系
- this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在对象的地址
- 在任何一个方法里都可以使用this指针。从本质上讲,c++ 中的对象其实是一种特殊的结构–出了变量,还包含着一些函数的特殊结构
- 在程序运行时,对象的属性(变量)和方法(函数)都是保存在内存里,这就意味着它们各自都有与之箱关联的地址
- 这些地址都可以通过指针来访问,而this指针时保存着对象本身的地址
- 因为静态方法不是属于某个特定的对象,而是由全体对象共享的,这就意味着它们无法访问this指针。所以,我们才无法在静态方法里访问非静态的类成员
- 在使用静态属性时,不要忘记为它们分配内存。只要在类声明的外部对静态属性做出声明(就像声明一个变量一样)
- 调用:
ClassName::methodName();
继承
类的继承
运用:可以创建一个类的堆叠层次结构,每个子类均将继承它的积累里定义的方法和属性。简单说,通过继承机制,可以对现有的代码进行扩展,并应用在新的程序中
基类:可以派生出其它的类,也称为父类或超类。
子类:子类是从基类派生出来的类。
- 方法->动作,属性->状态
例子
class SubClass:public SuperClass{...}
class Pig:public Animal{...}
继承机制中的构造器和解析器
构造器带着输入参数
//声明 class Animal{ public: Animal(std::string theName); std::string name; } class Pig:public Animal{ public: Pig(std::string theName); } //方法定义 Animal::Animal(std::string theName){ name = theName; } Pig::Pig(std::string theName):Animal(theName){ }
子类的构造器定义里的
:Animal(theName)
语法含义是:- 当调用Pig()构造器时(以 theName 作为输入参数),Animal()构造器也将被调用(theName 输入参数将传递给它)
- 当我们调用Pig pig(“小猪猪”);将把字符串"小猪猪"传递给Pig()和Animal(),赋值动作将实际发生在Animal()方法里
基类的构造器在使用子类构造器之前被调用
与基类构造器相反,基类的析构器将在子类的最后一条语句执行完毕后才被调用。
注意
- 初学者常犯的一个错误是用一个毫不相干的类去派生另一个毫不相干的子类
- 基本原则:基类和子类之间的关系应该自然和清晰
- 构造器的设计越简明越好!我们应该只用它来初始化各种有关的属性
- 基本原则:在设计、定义和使用一个类的时候,应该让它的每个组成部分简单到不能再简单
- 析构器的基本用途是对前面所做的事情进行清理
关于从基类继承来的方法和属性的保护:-class Pig : public Animal {…}
c++不仅允许对类里定义的方法和属性实施访问控制,还允许控制子类可以访问基类里的哪些方法和属性
public
- 是在告诉编译器:继承的方法和属性的访问级别不发生变化——即public仍可以被所有代码访问,protected只能由基类的子类访问,private只能由基类本身访问
protected
- 把基类的访问级别改为protected,如果原来是public的话,这将使得这个子类外部的代码无法通过子类去访问基类中的public
private
- 是在告诉编译器从基类继承来的每一个成员都当成private来对待,这意味着只有这个子类可以使用它从基类继承来的元素
覆盖方法
- 例如当我们需要在基类里提供一个通用的函数,但在它的某个子类里需要修改这个方法的实现,在c++中,覆盖(overriding)就可以做到
- 语法:在子类中声明并定义一个与基类中同名的成员
重载方法
- 重载机制使你可以定义多个同名的方法(函数),只是它们的输入参数必须不同
注意:
- 对方法(函数)进行重载一定要有的放矢,重载的方法(函数)越多,程序就越不容易看懂
- 在对方法进行覆盖(注意区分覆盖和重载)时一定要看仔细,因为只要声明的输入参数和返回值与原来不一致,你编写出来的就将是一个重载方法而不是覆盖方法。而且这种错误往往很难调试
- 对从基类继承来的方法进行重载,程序永远不会像你预期的那样工作
重载
函数的重载
定义:使用同样的函数名,定义一个有着不同参数,但有着同样用途的函数。可以时参数个数的不同,也可以是参数数据类型的不同
注意
- 对函数(方法)进行重载一定要谨慎
- 重载越多,程序越不容易看懂
- 注意区分重载和覆盖
- 我们只能通过不同参数进行重载,但不能通过不同的返回值重载(尽管后者也是一种区别
- 重载的目的:方便对不同数据类型进行同样的处理
运算符重载
运算符重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就会自动调用该函数,以实现相应的算法
运算符重载是通过定义函数实现的,运算符重载实际上是函数的重载
重载规则
c++不允许用户自己定义新的运算符,只能对已有的c++运算符进行重载
除了一下五个运算符不允许重载外,其它运算符允许重载
- 【.】成员访问运算符
- 【.*】成员指针访问运算符
- 【::】域运算符
- 【sizeof】尺寸运算符
- 【?:】条件运算符
重载不能改变运算符运算对象(操作数)个数
重载不能改变运算符的优先级别
重载不能改变运算符的结合性
重载运算符的函数不能有默认的参数
重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应该有一个是类对象或类对象的引用。(也就是说,参数不能全部是c++标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符性质)
运算符重载函数作为类友元函数
- 目的:为了访问类的私有成员
- 由于友元的使用会破坏类的封装,因此从原则上说,要尽量将运算符作为成员函数
重载运算符目的:让代码更容易阅读和理解
- 重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。Box operator+(const Box&);
重载«操作符
« 插入器
operator«()函数的原型
std::ostream&operator<<(std::ostream& os , Rational f );
- 第一个输入参数os是将要向他写数据的那个流,它是以“引用传递”方式传递的
- 第二个输入参数是打算写道那个流里的数据值,不同的operator«()重载函数就是因为这个输入参数才相互区别的
- 返回类型是ostream流的引用。一般来说,在调用operator«()重载函数时传递给它的是哪一个流,它返回的就应该是那个流的一个引用
多继承(multiple inheritance)
什么时候用多继承?
- 遇到的问题无法只用一个”是一个“关系描述的时候,就要用多继承
- 基本语法:
class TeachingStudent : public Student,public Teacher{...}
多态
多态性
多态性:指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作,从而实现“一个接口,多种方法”
多态是如何实现绑定的?
编译时的多态性:通过重载实现
- 编译时多态的特点是运行速度快
运行时的多态:通过虚函数实现
- 运行时多态的特点是高度灵活和抽象
虚方法(虚函数)
指针(以前的做法):创建一个变量,再把这个变量的地址赋值给一个指针。
问题:使用指向对象的指针
直接创建一个指针并让它指向新分配的内存块
int *pointer = new int; *pointer= 110; std :: cout << *pointer; delete pointer;
虚函数声明:只要在其原型前加上 virtual 保留字即可【virtual void play();】
虚函数:在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
- 我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
注意:虚方法是继承的,一旦在基类里把某个方法声明为虚方法,在子类里就不可能再把它声明为一个虚方法了
技巧
- 如果拿不准要不要把某个方法声明为虚方法,那么就把它声明为虚方法
- 在基类里把所有的方法都声明为虚方法会让最终生成的可执行代码的速度慢一些,但好处是可以一劳永逸地确保程序的行为符合你的预期
- 在实现一个多层次的类继承关系的时候,最顶级的基类应该只有虚方法
析构器都是虚方法是为了当一个基类的指针删除一个派生类的对象时,派生类的析构函数可以被正确调用
当类里有虚函数的时候,编译器会给类添加一个虚函数表,里面存放着虚函数指针。为了节省资源,只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数
虚继承(virtual inheritance)
- 通过虚继承某个基类,就是在告诉编译器:从当前这个类再派生出来的子类只能拥有那个基类的一个实例
- 虚继承语法:
class Teacher:virtual public Person{...}
抽象方法
- 抽象方法:把某个方法声明为一个抽象方法等于告诉编译器,这个方法必不可少,但我现在(在这个基类里)还不能为它提供一个实现
- 纯虚函数:【virtual void funtion1()=0;】 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
C++高级教程
链接和作用域
链接,当同时编译多个文件时:
g++ -o test main.cpp rational.cpp
- 每个源文件都被称为一个翻译单元(translation unit),在某一个翻译单元里定义的东西在另一个翻译单元里使用正是链接发挥作用的地方
存储类(storage class)
每个变量都有一个存储类,它决定着程序将把变量的值存储在计算上的哪个地方、如何存储,以及变量应该有着怎样的作用域
分类
默认的存储类是auto(自动)
自动变量储存称为栈(stack)的临时内存里并有着最小的作用域,当程序执行到语句块或函数末尾的有花括号时,它门将被系统回收(栈回收),不复存在
static 变量在程序的生命期内将一直保有它的值不会消亡,存储在静态存储区,生命周期为从申请到程序退出(和全局变量一样)
- 一个static 变量可以有external或internal链接
extern 它在有多个翻译单元时非常重要。它用来把另一个翻译单元里的某个变量声明为本翻译单元里的一个同名全局变量
- 编译器不会为extern变量分配内存,因为它在其它的地方已经分配过内存
register 它要求编译器把一个变量存储在CPU的寄存器里,但有着与自动变量相同的作用域
- register变量存储速度最快
用编译器建立程序
1、执行预处理器指令
- 把#include指令替换为相应的头文件里的代码
2、把 .cpp文件编译成 .o文件
- 把C++代码转换为一个编译目标文件,在这一步骤里,编译器将为文件里的变量分配必要的内存并进行各种错误检查
3、把 .o文件链接成一个可执行文件
- 如果只有一个C++源文件,步骤三只是增加一些标准库代码和生成一个可执行文件
- 但当同时编译多个源文件来生成一个可执行文件的时候,在编译好每一个组件之后,编译器还需要把它们链接在一起次才能生成最终的可执行文件
问题:当一个编译好的对象(即翻译单元)引用一个肯能不存在于另一个翻译单元里的东西时,潜在的混乱就开始出现了
链接分三种情况,凡是有名字的东西(函数,类,常量,变量,模板,命名空间)必然属于其中之一:外链接(external),内链接(internal)和无链接(none)
- 外链接:每个翻译单元都可以访问这个东西(前提时只要它知道这个东西存在)。普通的函数,变量,模板和命名空间都有外链接
- 内链接:在某个翻译单元里定义的东西只能在翻译单元里使用,在任何函数以外定义的静态变量都有内链接
- 无链接:在函数里定义的变量只存在于该函数的内部,根本没有任何链接
容器和算法
在C++标准库里面有许多现成的容器,它们都经过了老一辈精心设计和测试,可以直接拿来用
C++标准库提供的向量(vector)类型从根本上解决了数组先天不足的问题
std::vector<type>vectorName;
向量容器:向量可以动态地随着添加元素而无限增大(前提是有足够的可用内存)
- 可以用它的size()方法查知向量的当前长度(它包含的元素个数)
- 用push_back()方法来添加元素
- 还可以用访问数组元素的语法来访问某个给定向量里的元素
迭代器
- 遍历向量允许使用下标访问符来访问它的各个元素:nemes[x]
- 迭代器也可以遍历容器里的各个元素
- 迭代器是一种功能非常有限却很实用的函数,提供一些基本操作符:*、++、==、!=、=
- 迭代器是个智能指针,具有遍历复杂数据结构的能力,每种容器都支持
- 通过使用迭代器,当在程序里改用另一种容器的时候就不用修改那么多代码了
- 每种容器都必须提供自己的迭代器,事实上每种容器都将其迭代器以嵌套的方式定义于内部
- 因此各种迭代器的接口相同,型号却不同,这就是所谓泛型程序设计的概念:所有操作行为都使用相同接口,虽然它们的具体实现不同
- 好处:迭代器可以和所有的容器配合使用,而使用迭代器去访问容器元素的算法可以和任何一种容器配合使用
错误处理及调试
编译时错误
培养并保持一种编程风格
认真对待编译器给出的错误/警告信息
三思而后行
- 开始写代码前先画流程图
- 编译错误不要立刻修改源代码,应该先完整地审阅一遍源代码,再开始纠正错误
注意检查最基本地语法
把可能有问题地代码行改为注释
换一个环境或开发工具
检查自己是否已经把所有必要的头文件全部include进来
留意变量的作用域和命名空间
休息一下
使用调试工具
把调试好的代码另外保存起来并不再改动它,然后把代码划分成各个模块,用它们来搭建新的应用程序。
运行时错误
- 培养并保持一种良好的编程风格
- 多用注释,用好注释
- 注意操作符的优先级
- 不要忘记对用户输入和文件输入进行合法性检查
- 不要做任何假设
- 把程序划分成一些比较小的单元模块来测试
让函数返回错误代码
创建一些测试函数:专门测试某种条件并根据测试结果返回一个代码来表示当前函数的执行状态
- climits头文件把每种数据类型的最大值和最小值都分别定义为一个常量供我们比较 ,SHORT_MAX
assert函数
- 专门为调试准备的工具函数,被包含在C语言的assert.h库文件内,包含到C++里用#include
- assert()函数需要有一个参数,它将测试这个输入参数的真or假状态
- assert()可以用在某个程序里的关键假设不成立时立即停止该程序的执行并报错
- 除了assert()函数,还可以用cout语句来报告在程序里正在发生的事情
- 原则:最终用户看到的错误信息应该既专业有清晰,不能轻易中断程序,不能充满技术细节
- 专门为调试准备的工具函数,被包含在C语言的assert.h库文件内,包含到C++里用#include
捕获异常
异常(exception)就是与预期不相符的反常现象
基本使用思路
- 1.安排一些C++代码(try 语句)去尝试某件事,尤其是那些可能会失败的事
- 2.如果发生问题,就抛出一个异常(throw语句)
- 3.在安排一些代码(catch语句)去捕获这个异常并进行相应的处理
基本语法
try { //Do something. //Throw an exception on error. } catch { //Do whatever. }
注意:每条try语句至少要有一条配对的catch语句,必须定义catch语句以便让它接收一个特定类型的参数
C++还允许我们定义多条catch语句,让每条catch语句分别对应着一种可能的异常
catch(int e){...}
catch(bool e){...}
catch(...){...}
最后一条catch语句可以捕获任何类型的异常
在程序里,我们可以用throw保留字来抛出一个异常:throw1;
在某个try语句块里执行过throw语句,它后面的所有语句(截止到这个try语句块末尾)将永远不会被执行
与使用一个条件语句或return语句相比,采用异常处理机制的好处是它可以把程序的正常功能与逻辑与出错处理部分清晰地划分开来而不是让他们混在一起
定义一个函数时可以明确地表明你想让它抛出哪种类型地异常
type functionName(arguments)throw(type)
- 如果没有使用这种语法来定义函数,就意味着函数可以抛出任意类型的异常
TIPS
使用异常的基本原则:应该只用它们来处理确实可能不整常的情况
- 在构造器和析构器里不应该使用异常
- 如果try语句块无法找到一个与之匹配的 catch 语句块,它抛出的异常将中止程序的执行
在C++标准库里有个名为 exception 的文件,该文件声明了一个 exception 的基类,可以用这个基类来创建个人的子类以管理异常
如此抛出和捕获的是 exception 类或其子类的对象
如果你打算使用对象作为异常,请记住这样一个原则:以“值传递”方式抛出对象,以“引用传递”方式捕获对象
动态内存
动态内存管理
动态内存支持创建和使用种种能够根据具体需要扩大和缩小的数据结构,它们只受限于计算机硬件的内存总量和系统特殊约束
静态内存:变量(包括指针变量)、固定长度的数组、某给定的对象,指内存块的长度在程序编译时被设定为一个固定的值,而这个值无法改变
动态内存是由一些没有名字、只有地址的内存块构成,那些内存块是在【程序运行期间】动态分配的
new
int *i = new int;
delete i;
i = NULL;
从内存池申请一些内存需要用new语句,它将根据你提供的数据类型分配一块大小适当的内存
- 申请成功,new语句将返回新分配地址块的起始地址
- 申请失败,new语句将抛出 std::bad_alloc 异常
注意在使用完内存块后,应用 delete语句 把它还给内存池。另外作为一种附加的保险措施,在释放了内存块之后还应该把与之关联的指针设置为 NULL
NULL 指针
- 当把一个指针变量设置为 NULL 时,它的含义是那个指针将不再指向任何东西
new 语句返回的内存块很可能充满“垃圾“数据,所以我们通常先往里面鞋一些东西覆盖,再访问它们,或者在类直接写一个构造器来初始化
原则:每条 new 语句都必须与之配对的 delete语句,没有或者有两个 delete语句都属于编程漏洞
为对象分配内存
为对象分配内存和为各种基本数据类型(int,char,float)分配内存在做法上完全一样
- 用new向内存池申请内存
- 用delete来释放内存
注意
- 把方法声明为虚方法
- 在重新使用某个指针之前要调用delete语句,如果不这样做,那个指针将得到一个新内存块的地址,而程序将永远也无法释放原先那个内存块,应为它的地址已经被覆盖掉了
- delete语句只释放给定指针变量正指向的内存块,不影响这个指针。在执行delete语句之后,那个内存块被释放了,但指针变量还依然健在
动态数组
数组名和下标操作符[ ]的组合可以被替换成一个指向该数组的基地址的指针和对应的指针运算
建立一个动态数组
- 把一个数组声明传递给 new 语句将使它返回一个该数组基类型的指针
- 把数组下标操作符和该指针变量的名字搭配使用就可以像对待一个数组那样使用new语句为这个数组那样使用 new 语句为这个数组分配的内存块
删除一个动态数组
- 用来保存数组地址的变量只是一个简单的指针,所以需要明确地告诉编译器它应该删除一个数组
- 做法:在delete保留字地后面加上一对方括号:delete[]x;
从函数或方法返回内存
动态内存的另一个常见用途是让函数申请并返回一个指向内存块地指针
基本思路
在函数里调用 new 语句为某种对象或某种基本数据类型分配一块内存,再把那块内存的地址返回给程序的主代码,主代码将使用那块内存并再完成有关操作后立刻释放
变量作用域的概念:函数或方法有它们自己的变量,这些变量只能在这个函数的内部使用,这些变量我们称为局部变量(local variable)
为什么不应该让函数返回一个指向局部变量的指针?
- 任何一个函数都不应该把它自己的局部变量则指针作为它的返回值,因为局部变量在栈里,函数结束自动会释放
- 如果你想让一个函数在不会留下任何隐患的情况下返回一个指针,那它只能是一个动态分配的内存块的基地址
避免内存泄露
编程漏洞被称为内存泄漏(memory leak)
- new语句所返回的地址时访问这个内存块的唯一线索,同时也是delete语用来把这个内存块归还给内存池的唯一线索
情况一:new的地址值丢失了
int *x; x=new int [3000]; x=new int [4000]; delete[]x; x=NULL;
情况二:用来保存内存块地址的指针变量作用域的问题
void foo() { My Class *x; x = new MyClass(); }
- 解决一:在函数返回(结束)前delete x;
- 解决二:让函数返回内存块的地址
内存作用域
变量都有一个作用域:规定了它们可以在程序的哪些部分使用
- 全局作用域:把变量定义在函数的外部,它可以整个程序的所有函数里使用
动态内存,没有作用域,一旦被分配,内存块可以在程序的任何地方使用
- 需要跟踪它们的使用情况,并在不需要用到它们时把它们及时归还给系统
- 但是用来保存其地址的指针变量是受作用域的影响
命名空间和模块化编程
模块化(modularizat)
- 把程序划分为多个组成部分
- 通过把程序代码分散到多个文件里,等编译程序时再把那些文件重新组合在一起实现的
命名空间(namespace)
头文件
借助C++的预编译和编译器的能力,把一个复杂的应用程序划分成多个不同文件,而仍保持它在类和功能上的完整
头文件的基本用途是提供必要的函数声明和类声明
- 系统头文件:定义系统级功能,要使用这些功能就必须要把相应的头文件包含过来
- 自定义头文件 #include"fishc.h"
头文件是一些以.h作为扩展名的标准文本文件,一般情况下,都应该把自定义的头文件和其余的程序文件放在同一个子目录里,或者在主程序目录下专门创建一个子文件夹来集中存放它们
用头文件来保存程序的任何一段代码,如函数或类的声明,但一定不要用头文件来保存它的定义(实现)
头文件里应该注释说明:创建日期,文件用途,创建者姓名,最后一次修改日期,有什么限制,前提条件。另外头文件里的每一个类和函数也应该有说明
提示
- 头文件经典的做法是只保存函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量
- 头文件应该只包含最必要的代码,比如只声明一个类或只包含一组彼此相关的函数
使用
- 在创建了头文件后,用双引号引用文件名
#include"fishc.h"
- 如果没有给出路径名,编译器将到当前子目录以及当前开发环境中的其他逻辑子目录里去寻找头文件
- 导入头文件可以用相对路径
#include"./fishc.h"
- 如果头文件位于某个下级子目录里,那么以下级子目录的名字开头
#include "includes/fishc.h"
- 如果头文件位于某个与当前子目录平行的”兄弟“子目录里
#include "../includes/fishc.h"
- 在创建了头文件后,用双引号引用文件名
创建实现文件
代码模块化规则:接口(函数的原型)和实现(函数体的定义)分开
头文件的重要性不仅体现在它们可以告诉编译器某个类、结构或函数将有怎样的行为,还体现在它们可以把这些消息告诉给程序员。
C++预处理器
- #if —如果表达式为真,执行代码
- #else —如果前面的 #if 表达式为假,执行代码
- #elif —相当于”elseif“
- #endif —用来标志一个条件指令的结束
- #ifdef —如果本指令所引用的定义已存在,执行代码
- #ifndef —如果本指令所引用的定义不存在,执行代码
- 格式 #if //代码 #endif
- #ifndef LOVE_FISHC #define LOVE_FISHC #endif 如果 LOVE_FISHC 还没有定义则定义它
命名空间
创建的每一个类、函数和变量都只能在一定的区域内使用
最大的区域是全局作用域,最小的区域是一个代码块
命名空间就是由用户定义的范围,同一个命名空间里的东西只要在这个命名空间有独一无二的名字就行
创建命名空间
namespace myNamespace { //全部东西 } //注意在最末尾不需要加分号
如果某个东西在命名空间里定义的,程序将不能立刻使用它
意义:把东西放在它们自己的小盒子里,不让他们域可能有着相同名字的其它东西发生冲突
使用命名空间方法
- 方法一:
std::cout<<
- 方法二:
using namespace std;
cout<<"";
- 方法三:
using std::cout;
cout<<"";
- 注意:using 指令的出现位置决定着从命名空间里提取出来的东西能在哪个作用域内使用
- 如果 using 放在所有函数前面,它将拥有全局性,如果你把它放在某个函数里,那它将旨在这一个函数里使用
- 方法一:
模板
函数模板
模板可以没有任何类型:它们可以处理的数据并不仅限于某种特定的数据类型
当程序需要用到这些函数中的某一个时,编译器将根据即时生成一个能够对特定数据类型进行处理的代码版本
泛型编程技术可以让程序员用一个解决方案解决多个问题
STL库
定义函数模板
template<class T> void foo(T param) { //do something }
- 第一行代码里,在尖括号里有一个class T,用来告诉编译器:字母T将在接下来的函数里代表一种不确定的数据类型
- 关键字class并不意味着这个是类,只是一种约定俗成的写法
- 在告诉计算机 T 是一种类型之后,就可以像对待一种普通数据类型那样使用它了
注意:
创建模板时,还可以用
template<Typename T>
来代替template<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作为占位符即可
高级强制类型转换
传统的强制类型转换:把需要的指针类型放在一对圆括号之间,然后写出将被强制转换的地址值
Company *company = new Company(“APPLE”,“Iphone”);
TechCompany *techCompany = company;
- 注意:不能既删除company,又删除tecCompany。因为强制类型转换操作不会创建一个副本拷贝,它只告诉编译器把有关变量解释为另一种类型组合形式,所以他们指向的是同一个地址
万一被强制转换的类型和目标类型结构完全不同,怎么办?
强制类型转换操作符
conset_cast<MyClass*>(value)
- 用来改变value的“常量性”
dynamic_cast<MyClass*>(value)
- 用来把一种类型的对象指针安全地强制转换为另一种类型的对象指针。注意:如果value的类型不是一个MyClass类(或MyClass的子类)的指针,这个操作将返回NULL
reinterpret_case<T>(value)
- 在不进行任何实质性的转换的情况下,把一种类型的指针解释为另一种类型的指针或把一种整数解释为另一种整数
static_case<T>(value)
用来进行强制类型转换而不做任何运行时检查,老式强制类型转换操作的替代品
疑难杂症
用cin输入时如何让不跳过任意地方的空格和换行?
- 操作符noskipws会令输入运算符读取空白符,而不是跳过它们。
- cin»noskipws;//设置cin读取空白符
- cin»skipws;//将cin恢复到默认状态,从而丢弃空白符
函数引用传参
- &a,a是实参,即传递过来的那个变量。该变 a 其变量也会变
- 数组 array[6]; swap(int &a,int n); 当a是数组的第一个变量时,可以用*(&a+1)来访问第二个变量
nullptr与NULL
C++不允许void*隐式转换成其它类型的指针
#ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif
然而这样用NULL代替0表示空指针在函数重载时会出现问题
为解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针,使用nullptr作为实参。