本文共 12055 字,大约阅读时间需要 40 分钟。
字符串常量按字符书写顺序依次存储在内存中,并在最后存放空字符’\0’表示字符串常量的结束。ASCII字符在内存中占1个字节,而中文字符占2个字节
有名常量是指用关键字const
修饰的变量。由于该变量只能读取,而不能被修改,所以 也称为常变量。有名常量必须在定义时进行初始化,之后不再允许赋值。例如:
const double PI=3.1415926;const int Max=1000;
有名常量与变量一样,存储在程序的数据区中,可以按地址进行访问。变量在初始化之后还可以对其进行修改,但对有名常量的任何修改都会引发编译器报错。
使用有名常量的好处在于:
便于程序的维护——假设程序中多处用到圆周率,如果需要提高它的精度,则只需在有名常量的定义处修改即可。对于大型软件,程序的可读性和可维护性是两个极其重要的评价指标。
位运算的操作数只能是bool
、char
、short
或int
类型数值,不能是float
和double
实型数。支持的运算有按位取反(~
)、左移(<<
)、 右移(>>
)、按位与(&
)、按位或(|
)和按位异或(^
)。有符号数时,向左移动n位,丢弃左边n位数据,并在右边填充0,同时把最高位作为符号位;向右移动n位,丢弃右边n位数据,而左边正数补0,负数补1。
不能用变量来定义数组大小
指针变量与整数相加或相减的结果是指针前移或后移若干个单元,单元大小为sizeof(type)
在输入输出语句中插入hex
(十六进制)、oct
(八进制)和dec
(十进制)指明输入输出数据认定的格式。例如:
//以十六进制输入数据//若输入f 11,则x和y的值分别为15和17cin>>hex>>x>>y;//hex为十六进制格式控制符,输出100//设置过hex后,整数均以十六进制格式输出,除非用oct或dec重新设置cout<<<256<
strcpy(参数1,参数2)
是系统提供的函数,其功能是将参数2的内容复制到参数1所指定的字符数组中。例如://初始化的时候可以直接赋值char str[20] = "";strcpy(str, "星期日");
跳转语句
break
:在循环语句中,break
语句的作用是终止循环,流程跳转至循环语句之后。需要注意的是,对于循环嵌套语句,如果break
语句是在内循环中,则其只能终止其所在的循环语句的执行,流程跳转至外循环。continue
:其功能是将流程跳转至当前循环语句的条件表达式处,判断是否继续进行循环。continue
语句与break
语句的区别是:continue
语句是终止本轮循环,而break
语句是终止本层循环。此外,continue
语句只能用在循环语句中。
^
表示按位异或
C++容许在函数定义时为形参指定默认值。默认值的指定遵守“从右到左连续定义”的规则。例如:
double max(double a, double b=0, double c=0);
某些情况下,我们需要修改实参的值,而某些情况下,我们不想修改实参的值:
内联函数:通常inline
限定符只用于那些非常小并且被频繁使用的函数。例如:
inline bool isNumber(char ch) { return ch>=’0’ && ch<=’9’?true:false;}
C++编译器在生成程序时是将反映对象特征的数据成员分开,独立保存于程序的数据存储区域,而在程序的代码区仅保存一份成员函数。也就是说,物理上对象的数据成员和成员函数是分离的,并且成员函数是分享的。
程序在生成过程中,在类的成员函数形参表的最前端,编译器为其添加一个指向对象的指针,并命名该形参名为this
,称为this
指针。当通过对象调用成员函数时,系统将对象的地址传递给所调用成员函数的this
指针,从而实现对象与成员函数的正确绑定。
类的对象在逻辑上是相互独立的。在物理上,对象的数据是独立的,不同的对象拥有不同的数据,但是,类的成员函数却只有一份,为类的所有对象共有。
如果有参的构造函数的所有形参都指定了默认值,那么该构造函数即可充当默认构造函数的角色。此时,不需要(其实也不能)再定义默认构造函数。如果再定义默认构造函数,同样会被视为类中有两个默认构造函数,引发调用匹配错误。
拷贝构造函数的形参只能说明为类的对象的引用,如
Student(Student&);
拷贝构造函数是在对象被复制时被调用,如果拷贝构造函数是以传值方式传递实参,由于在调用类的拷贝构造函数时,实参要被复制给形参,这种复制的结果就是导致再一次调用该类的拷贝构造函数,产生无穷的递归调用。
为避免在拷贝构造函数中不小心改变了原对象中的数据成员,通常在拷贝构造函数的形参前加上const
修饰符。
//类中声明Student(const Student&);
构造函数的调用顺序与定义顺序一致。对象调用析构函数的顺序正好与构造函数的调用顺序相反。这是由于这些对象都存储在程序的栈区,先定义的对象先被压栈,而销毁的过程与对象从栈中弹出的顺序一致。
类的复用技术——组合
Class Point { private: int x; int y;};Class Circle {private: double radius; Point center;};
从构造函数Circle(int a,int b,double r):center(a,b),radius(r){}
的设计可知,成员初始化列表不仅能对成员对象进行初始化,而且也可以用于对普通数据成员赋初值。对于普通的数据成员即可以在构造函数的函数中对其赋值,也可以在成员初始化列表中完成,但对于对象成员却只能在列表。对象成员构造函数的调用先于类的构造函数。
Circle(int a,int b,double r) { center=Point(a,b); radius=r;}
上面的构造函数虽然也完成了对象的拷贝功能,但由于其没有在成员初始化列表中明确成员对象的赋值,因此先调用了成员对象的默认拷贝函数对其赋初值。实际完成赋值任务的语句是center=Point(a,b);
,该语句先生成一临时无名对象,再调用系统提供的赋值功能把临时无名对象的数据成员值拷贝给对象的对象成员center
,之后临时无名对象被销毁。
含有对象成员的类在对其对象初始化时,构造函数是先调用对象成员的构造函数对成员对象初始化。调用顺序与成员对象在初始化列表中的次序无关,与其在类中声明的次序一致。
类的静态数据成员与全局变量相比具有两个优点:
对于非静态数据成员,每个类的对象都有自己独立的数据部分,而静态数据成员对类的所有对象只有一份,保存在程序的数据区。
无论类的对象定义与否,类的静态数据成员都存在。在类中,静态数据成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏数据隐藏的原则,保证了数据的安全性。
静态数据成员是在类定义中用static
关键字修饰的数据成员,静态数据成员的初始化与一般数据成员初始化不同。
类的静态数据成员属于类。即使在程序中没有定义类的对象,类的静态数据成员也会在数据区生成并被初始化,因此无论类的对象是否已定义,类的静态数据成员在程序加载时生成。
class staticMemberExample {private: int no; static int total;};//在类外对静态数据成员进行初始化,注意:前面不加staticint staticMemberExample::total=0;
对象的静态数据成员和普通数据成员存储在不同的区域,前者在数据区,后者在栈区。从sizeof(object)
的值为4可知,对象object
中仅存储了对象的no
信息,因为int
类型数据的大小就是4。对象的静态数据成员与普通数据成员在内存中的存储位置不同,前者与全局变量相同,在数据区,后者可在栈或自由存储区。
类的静态函数成员属于类,与类的对象无关。即使在程序中没有定义类的对象,也可以通过类名直接调用静态成员函数。静态成员函数无法访问类的非静态数据成员,也不能直接调用类的非静态成员函数,只能访问静态数据成员和调用其他的静态成员函数。若要访问类中非静态的成员时,必须通过函数参数传递类的对象给静态成员函数,通过对象才能访问非静态成员(数据成员和成员函数)。
<类名> :: <静念成员函数名> (实参表); <类的对象> . <静态成员函数名> (实参表); 静态成员函数名> 类的对象> 静念成员函数名> 类名>
类的静态成员函数提供了一种访问静态数据成员的方式。此外,它还避免使用全局函数,为函数设置了一个类域的访问权限。
类的静态成员函数可以在类内定义,也可以在类外定义。在类外定义时,不能再用static
关键字作为其前缀。
类的友元不是类的成员,但如同类的成员函数一样,它可以访问类的私有或保护的成员。类的友元分为友元函数和友元类。
友元函数是类中用关键字friend
修饰的非成员函数,该函数可以是普通函数,也可以是另一个类的成员函数。其在类中的声明格式如下:
friend <返回类型> [ <类名> ::] <函数名> ([形参表]); 函数名> 类名> 返回类型>
C++中,友元函数的主要用途是重载运算符和生成迭代器类,以及用友元函数同时访问两个或多个类的私有数据,使程序的逻辑关系更清晰,其余情况应慎用友元函数。
Point& Point::setX(double a) { X=a; //便于“瀑布式”调用 return*this;}
Point
类中的setX
和setY
函数均返回了*this
,即对象自身。在主函数中利用该设计实现了所谓的“瀑布式”调用point2.setX(1).setY(20);
。
在类中声明另一个类是该类的友元类,则友元类中的所有成员函数都是该类的友元函数,可以访问类的所有成员。
friend class <类名> ; 类名>
类的友元关系是单向的,不具有传递性。类A
是类B
的友元类,并不意味着类B
一定是类A
的友元类,除非在类A
中也声明B
是友元类。同样,如果类A
是类B
的友元类,类B
又是类C
的友元类,并不能确定类A
也是类C
的友元类,友元关系不传递。类的友元关系不被继承,也就是说派生类不继承类的友元关系。
<返回类型> <类名> ::operator <运算符> ( <形参表> ){ <函数体> } 函数体> 形参表> 运算符> 类名> 返回类型>
对于双目运算符,<形参表>
中应有一个形参,以当前对象作为左操作数,而形参为右操作数。对于单目运算,是以当前对象为操作数,<形参表>
中无形参。
对于“++
”和“--
”运算符,分为前置和后置运算。为能正确区别二者,C++规定用无参成员函数格式表示实现前置运算,用带一个int
形参的函数表示实现后置运算,这里的形参不起任何作用。
由于成员函数隐含的第1个参数是this
指针,运算符重载函数的左操作数只能是对象,不可能是实数,解决方法是采用友元函数重载运算符。
实际上,形参const Complex&
与complex&
虽然仅一字之差,但实现方式是不一样的。Complex&
形参是引用传递,实现时系统是将引用对象的地址压入调用堆栈中。const Complex&
由于是const
引用,禁止修改被引用对象。为防止修改,编译器在实现const
引用时,生成无名临时对象供调用函数访问。事实上,系统在引用const
对象时,访问的是一个由系统产生的复制品。
在引用const
对象时,系统会调用构造函数生成无名临时对象。利用该技术可以把实数传给const
引用复数类型形参,再由构造函数生成临时复数对象。
friend Complex operator+(const Complex&c1,const Complex&c2);
后置++
运算符重载函数的返回类型声明为const Complex&
,加const
的目的是阻止对对象的连续后置++
操作。
赋值运算符=
、类型转换运算符<类型>()
、下标运算符[]
和函数调用运算符()
,它们都只能重载为成员函数。
Merchandise myGood2=myGoodl;
中的等号并不调用赋值运算符重载函数,它等价于Merchandise myGood2(myGoodl);
,是通过调用拷贝构造函数实现对象复制。
文件包含指令的作用是用指定的文件内容替换该指令,其中尖括号<>
表示在系统目录的include
子目录下寻找该文件,而双引号""
表示先在当前文件所在的目录下查找,如果找不到,再到系统指定的文件目录下寻找。
C++的编译预处理指令主要有文件包含指令(#include
)、宏定义(#define
)以及条件编译指令。所有预处理指令都以“#
”开头,以回车符结束,且每条指令单独占一行。
#defime max(a,b) (((a)>(b))?(a):(b))
需要注意的是,宏替换可能产生错误。如果将上式写成#define max(a,b)a>b?a:b
,则语句10+max(x,y)+5
被替换成10+x>y:?x:y+5
,结果错误。
#undef<宏名>
的含义是:取消某个宏名的定义,其后的<宏名>
不再进行替换和不再有宏定义。
条件编泽主要用于编译预处理器根据某个条件满足与否来确定某一段代码是否参与编译。常用的条件编译指令包括#if
、#else
、#elif
、#ifdef
、#ifndef
、#endif
等。
int* ap
改为int ap[]
含义相同,系统视其为int*
类型void show(int* ap,int size){}
C++程序在运行时,其所占用的内存空间被划分为4个区域:代码区、全局数据区、栈区和自由存储区。函数中使用的局部变量多数都分配在栈区,静态变量和全局变量被存储在全局数据区。自由存储区又称为堆区,是由程序员根据需要进行分配和释放的内存区域。
在delete
运算符中的方括号[]
是告诉编译器回收整个数组所占用的内存空间,并且方括号中不需要填写数组的元素数。例如:
int *ptr,n;//一维数组的动态分配。数组元素的个数可以是变量ptr=new int[n];//ptrB是指向int[20]数组类型的指针int (*ptrB)[20];//ptrB指向行数为n,列数为20的二维数组ptrB=new int[n][20];//回收一维数组int[n]的内存空间delete []ptr;//回收二维数组int[n][20]的内存空间delete []ptrB;
ptr
在栈中,所指向的单元在堆中。
对于没有提供拷贝构造函数或赋值运算符重载函数的类,系统将提供一个默认的对应函数。系统所提供的函数只是简单地实现两个对象数据成员的复制,对于没有使用堆区的类,这样的函数能很好地完成复制任务。对于使用了自由存储区的类,如果仅是简单地对指针变量进行赋值操作,则会导致两个甚至更多对象中指向堆区的指针指向同一块内存,即所谓的浅复制。
深复制是复制一个完整且独立的对象的副本,其实质是每个对象都应拥有自己独立的堆空间,并且通过复制保持两个内存区域的内容一致。
const
修饰符在C++程序中用途较广,其主要作用是防止对变量或对象的修改操作。const Array& operator=(const Array&)
函数形参中的const
是防止传递的对象被修改,函数返回类型中的const
是禁止修改函数返回的对象。函数bool operator==(const Array&) const
后面的const
是指该成员函数不能修改类中的任何数据成员,其实质是为成员函数中由编译器为之增加的隐式形参this
指针添加const
修饰。
由于递归在实现中存在大量的函数调用,而函数调用会带来参数压栈和弹栈的开销,因此,递归函数在运行过程中的内存空间占用和机时开销高于非递归方式,代码的运行效率相对较低。
函数指针
如同数组名是数组的首地址一样,函数名代表的是该函数的首地址,也就是函数执行代码的入口地址。
void sort(int n,double array[]);void (*funPtr)(int,double[])=sort;
函数指针funPtr
指向sort
函数。这里,所指函数的形参和函数返回类型需要完全匹配。在函数首地址赋给函数指针时,既可在函数名前添加取地址运算符“&
”,也可以不加。
函数本身不能作为函数的形参,但函数指针可以。利用函数指针可以将函数作为实参传递给另一个函数。
typedef
关键字含义是“类型定义”,作用是将一种数据类型定义为某一个标识符,在程序中可使用该标识符来实现相应数据类型变量的定义。typedef
能简化较为复杂类型的声明,用有明确意义的标识符代替,增强程序的可读性。public
)、私有继承(private
)和保护继承(protected
)。默认的继承方式是私有继承,即不指明继承方式等同于私有继承。class <派生类名> :[ <继承方式1> ] <基类名1> ,...,[ <继承方式n> ][ <基类名n> ]{ <派生类的成员> }; 派生类的成员> 基类名n> 继承方式n> 基类名1> 继承方式1> 派生类名>
C++中下列特殊的成员函数不被派生类所继承:
私有的成员函数不能被继承的原因十分自然,因为它仅属于基类,在派生类中也不能直接访问它,否则破坏了基类的封装性(友元类与友元函数是以牺牲封装性为代价换取性能)。构造函数、析构函数和赋值运算符重载函数不被继承的主要原因是基类的对应函数不能处理在派生类引入的新的数据成员,不能完全正确地完成相应的功能(只能正确地处理基类的数据成员)。
对继承到派生类中基类成员的修改包括两个方面:
Override
)。无论采用什么样的继承方式,基类中的所有成员均被派生类所继承,派生类对象一定含有基类的数据成员和成员函数。继承方式仅仅影响到基类成员在派生类中的访问控制属性。对于在派生类中不可直接访问的私有成员,正确的方法是通过基类提供的公有成员函数进行问接的访问。
在派生类中重新定义基类的同名成员函数后,基类中的同名成员函数将被同名覆盖(Override
)或隐藏(Hide
)。
同名覆盖是由于派生类与基类的同名成员函数的函数签名相同,派生类对象在调用同名成员函数时,系统调用派生类的同名成员函数,而基类的相应函数被遮盖。
隐藏是由于派生类与基类的同名成员函数的函数签名不同(即函数名相同而形参不同),派生类对象在调用同名成员函数时,系统只往派生类中查找,不再深入到基类。派生类的同名成员函数阻止了对基类中同名函数的访问。
函数重载只能出现在同一个类中,基类与派生类的同名函数之问不存在重载关系。
编泽器调用类的成员函数的方法是:根据函数名(不是函数签名)沿着类的继承链逐级向上查找相匹配的函数定义。
如果在类层次结构的某个类中找到了同名的成员函数,则停止查找,否则沿着继承链向上继续查找。派生中的同名函数阻止编译器到其基类继续查找,这就是出现同名覆盖和隐藏现象的原因。
(1)在派生类中没有找到成员函数,再到基类中查找。如果在基类中找到并且实参与形参正确匹配,则函数调用成功,否则出错。
(2)在派生类中找到了同名的成员函数,不再到基类中查找。此时又有两种情况:
在派生类中可利用作用域标识符(::
)直接调用基类的同名成员函数。
派生类对象向基类对象、指针或引用赋值满足以下兼容规则:
在派生类中定义正确的转换构造函数或赋值运算符重载函数,则能确保将基类对象赋给派生类对象语句通过编译。此时,派生类对象中数据成员的内容与所定义的构造函数或赋值运算符重载函数相关。
用强制类型转换运算符转换基类对象为派生类对象并赋给派生类指针或引用,格式如下:
<派生类对象指针> = static_cast <派生类*> (& <基类对象> ); <派生类> & <派生类引用> = static_cast <派生类&> ( <基类对象> ); 基类对象> 派生类&> 派生类引用> 派生类> 基类对象> 派生类*> 派生类对象指针>
用static_cast
运算符能实现类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。这种转换分为“上行”和“下行”两种。上行转换是指把派生类指针或引用转换成基类指针或引用,是安全的;下行转换是指把基类指针或引用转换成派生类指针或引用,是不安全的。
在创建派生类对象时,构造函数的调用顺序为:
派生类对象在撤消时是按照构造函数调用相反的次序调用类的析构函数。
商品类的数据成员price
分别通过手机类和播放器类派生给音乐手机类,同样的数据成员在音乐手机派生类对象中将出现两个,并且存储地址也不相同。这样不仅浪费存储空问,而且还会因为需要维护数据的一致性增加额外的开销。
C++语言通过引入虚基类(Virtual Base Class)来支持派生类对象在内存中仅有基类数据成员的一份拷贝,以消除钻石继承所产生的数据重复存储问题。
MusicPhone
类的构造函数在定义时需要显式地说明调用虚基类Merchandise
的构造函数Merchandise(n,p)
,否则myObj
对象中name
和price
成员将不赋初值。虽然Merchanaise
类的直接派生类MobilePhone
和MusicPlayer
的构造函数均包含了对Merchandise
构造函数的调用,但在MusicPhone
类对象构造时不调用基类MobilePhone
和MusicPlayer
构造函数中说明的虚基类Merchandise
的构造函数。若Merchandise
不是虚基类,则Merchandise
构造函数将被调用二次(通过MobilePhone
和MusicPlayer
类的构造函数)。此时MusicPhone
类的构造函数定义时不再需要直接说明对Merchandise
类构造函数的调用。
从程序实现的角度,多态可分为两类:
编译时的多态性是通过静态绑定实现的,而运行时的多态性则是在程序运行过程中通过动态绑定实现的。
这里的绑定(Binding,又称联编)是指函数调用与执行代码之间关联的过程。静态绑定(Static Binding)是在程序的编译与连接时就已确定函数调用和执行该调用的函数之间的关联。在生成的可执行文件中,函数调用所关联执行的代码是已确定的,因此静态绑定也称为早绑定(Early Binding)。函数重载(含运算符重载)就属于编译时的多态。编译器在判定应当调用多个重载函数中哪一个时,是根据源程序中函数调用所传递的实参类型查找到与之相匹配的重载函数并连接。动态绑定(Dynamic Binding)是在程序运行时根据具体情况才能确定函数调用所关联的执行代码,因而也称为晚绑定(Late Binding)。
虚函数的使用应注意:
virtual
可以省略。在类的层次结构中,成员函数一旦在某个类中被声明为虚函数,那么在该类之后派生出来的新类中它都是虚函数;VC++处理动态绑定的基本方法是:编译器为拥有虚函数的类创建一个虚函数表,在对象中封装vfptr
指针,用于指向类的虚函数表‘vftable
’。虚函数表中存储了该类所拥有的虚函数的入口地址,即函数指针。如果派生类重新定义了基类的虚函数,那么虚函数表中保存的是指向该类虚函数的指针,否则保存的是其父类的对应虚函数指针。
在基类中定义其析构函数是虚函数,其所有派生类中的析构函数将都是虚函数,尽管它们的名称并不相同。如果对一个基类指针应用delete
运算符显式地销毁其类层次结构中的一个对象,则系统会依次调用派生类和基类的虚析构函数撤消各自创建的对象。
纯虚函数与抽象类
virtual <返回值> 函数名([( <形参表> )]=0; 形参表> 返回值>
含一个或多个纯虚函数的类称为抽象类(Abstract Class)。
尽管无法实例化抽象类的对象,但是程序可以定义抽象基类的指针或引用,并通过它们访问以其为基类的继承层次结构中的所有派生类的对象。
抽象基类中声明的纯虚函数是所有派生类的公共接口,在类继承层次结构中,不同层次的派生类可以提供不同的具体实现,但使用这些函数的方法则是一致的(用抽象基类的指针或引用访问)。
用函数模板可以实现一个不受数据类型限制的具有良好通用性的函数设计,其方法是在函数模板的形参表中用无类型的参数代替形参的数据类型,用函数模板生成可执行函数的过程是在程序编译时,编译器用具体的数据类型置换模板类型的形参,并对其进行严格的类型检查。
template <模板形式参数表> 返回类型 函数名(形式参数表){函数体} 模板形式参数表>
模板形式参数表的参数可以有多个,它们之间用逗号分隔。
模板类型形参是由关键字typename
或class
加标识符组成的。模板非类型形参的声明与普通函数的形参的声明相同。
templateT max(T ary[],int size);
模板类型形参T
的作用是指代任何系统内置的数据类型或用户定义类型。
函数模板的实例化(Instantiation)是指编译器根据函数调用时所传递的实参数据类型生成具体函数的过程。实例化的结果是生成能处理某种特定数据类型的函数实体,这种函数称为模板函数(Template Function)。
运算符重载在C++程序设计中非常重要。如果Student
类不支持“>
”和“<<
”运算符重载,则系统就不能应用函数模板于用户自定义的类类型,只能处理系统内置的数据类型,函数模板的应用范围将受到限制。
函数模板与函数模板重载。多个函数模板的函数名相同,但每个函数模板具有不同的形式参数。
函数模板与非模板函数重载。非模板函数与函数模板同名,但具有不同的函数形式参数。
编译器如果能匹配到普通函数完成一个函数的调用,则不再寻找函数模板来实例化一个模板函数实现函数调用。
类模板是设计线性表、栈、队列等基本数据结构的工具。
template <模板形式参数农> class类名{类成员;}; 模板形式参数农>
类模板中的形式参数表可以为形参指定默认值,如此可避免每次实例化时都要显式地给出实参。例如:
template
类模板的成员函数可以是函数模板,也可以是普通函数。
可用typedef
声明模板类为程序引入的新的数据类型,提高程序的可读性。
typedef TwoDimensionalArrayTwoDimArrayDouble;TwoDimArrayDouble myTDArray;