开始学c++(更新中)
1.include <iostream>
2.using namespace std;
3.面向对象编程OOP:类为其核心概念之一 类定义描述的是数据格式及其用法,对象则是根据数据格式规范创建的实体。
4.关于变量名:以两个下划线打头或以下划线和大写字母打头的名称倍保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。
5.修改进制表示:cout<<hex; dec十进制 hex十六进制 oct八进制 (在修改格式之前,原来的格式一直有效)(标识符hex等位于名称空间std中)
6.cout.put( )意思是通过类对象cout来使用函数put()
7.const限定符,相较于#define的优点(可以明确指明类型,可使用c++的作用域规则将定义限制在特定的函数或文件),在c++中可以使用const值来表明数组长度。(关于编程习惯:可以将常量的首字母大写以提醒这是个常量)
8.get( )和getline( ),都读入一行输入直到换行符,但getline丢弃换行符而get将换行符保留在输入序列中。例如:cin.getline(str,num)其中str为数组名,num为元素个数(包括\0);而使用get时换行符被保留,可使用get的变体不接受参数的get()来处理换行符,我们还可以使用拼接形如cin.get(str , 20).get(); 当然我们也可以这样cin.getline(str1,20).getline(str2,20)来分别读入数组str1和2,其效果和两次调用getline相同。
{之所以可以使用拼接,是因为cin.get()返回一个cin对象,该对象随后被用来调用get()函数}
9.cin>>读取后和换行符留在了输入队列,若之后要输入字符串要使用get()读取并丢弃换行符,如(cin>>a).get()或者不使用拼接。
10.将字符串作为一种数据类型的表示方法string,头文件<string>。而且可以将一个string对象赋值给另一个string对象(=),用+或者+=进行合并操作(附加到末尾)。
11.确定字符串长度的新方法size(),如:int len=str.size(),表明str是一个对象,size()是一个类方法,方法是一个函数只能通过所属类对象进行调用。由于istream中没用处理string对象的类方法,所以有getline(cin,str),将cin作为参数来指出去哪里查找参数,而对于字符数组为cin.getline(charr,20)
12.c++允许在声明结构变量时省略关键字struct。
13.使用new来分配内存,使用delete来释放内存但不会删除指针本身。例:int p=new int;delete p;或int *p=new int [10]; delete [ ] p ; new和delete要一一配对
14.int p="abc";若要打印地址要将指针强制转换为另一种指针类型(如int*),cout<<(int *) p;直接cout<<p;则会打印字符串
15.字符串数组的话可以用string指针而不用二维字符数组,如:string *p=new string [10];对p[num]进行操作,用cout<< p[num];进行打印。
16.自动储存:如果在其中一个代码块中定义了一个变量,则该变量仅在程序执行该代码块中代码时存在;静态储存:在函数外定义或声明变量时使用static;
自动储存和静态储存的关键在于:严格限制了变量的寿命,变量可能存在与程序的整个生命周期也可能只在执行特定函数时存在。
17.数组的替代品:vector(头文件vector)和array(头文件array)
vector:如vector<type_name>vt(n)创建名为vt的vector对象,它可以储存n个类型为type_name的元素,n可以是变量;
array:如array<type_name,n>arr创建名为arr的array对象,它包含n个type_name类型的元素,n不能是变量;
array并非只能储存基本数据类型,还可以储存类对象,如:
const int sea=4; const std::array<string,sea>names={"fw","sd","we","ef"};
使用了一个const array对象,该对象包含4个string对象。
18.数组,vector对象,array对象(c++11)的区别
都可使用标准数组表示法来访问各个元素;array对象和数组储存在栈中,vector对象储存在自动储存区或堆中;可以将array对象/vector对象赋给另一个array对象/vector对象;
可以对vector对象和array对象进行检测越界并禁止操作:使用成员函数at()如:a0.at(-1)=10;使用at()时将在运行期间捕获非法索引而程序默认中断,代价是运行时间更长。
19.前缀递增,前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合;
后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右结合。
20.可以使用关系运算符来比较string类字符串(如:==),条件是至少有一个操作数为string对象,另一个操作数可以是string对象也可以是C-风格字符串。
21.c++建立类型别名有预处理器(#define)和关键字typedef两种,而且typedef更优,typedef type_name new_name;即可,define是强行的字符代换,而typedef是为已有的类型建立一个新名称,但不会创建新类型
22.cin在读取char值时将省略空格和换行符,可以使用cin.get(ch)补救。
23.检测文件尾使用fail()若检测到EOF返回true否则返回false,很多PC编程环境将Ctrl+Z视为模拟的EOF。如:
char ch='i'; while(cin.fail()==false) { cin.get(ch); cout<<ch; }
24.cctype字符函数库:isalpha()来检查字符是否为字母字符,isdights()来测试字符是否为数字字符,isspace()来测试字符是否为空白,ispunct()来测试字符是否为标点符号。
25.写入到文本文件(文件输出):(头文件fstream)
1.包含文件头fstream;
2.创建一个ofstream对象;
3.将该ofstream对象同一个文件关联起来;
4.像使用cout一样使用该ofstream对象;
ofstream outFile; ofstream fout; outFile.open("fish.txt"); char filename[20]; cin>>filename; fout.open(filename); double wt=20.009; outFile<<wt; char line[20]="yes"; fout<<line<<endl;
程序使用完文件后,应该将其关闭
outFile .close();
但不需要文件名作参数,因为已经同特定文件关联起来了。
注意:若在程序运行之前文件不存在,方法open()将会创建一个;若文件已经存在,打开已有的文件以接受输出时,默认将它的长度截短为零——原来的内容将会丢失。
25.读取文本文件(文本文件输入):
和输出极相似
ifstream inFile; ifstream fin; inFile.open("bowling.txt"); char filename[20]; cin<<filename; fin.open(filename); double wt=20.009; inFile>>wt; char line[20]; fin.getline(filename,20);
检查文件是否被打开的首先方法是使用方法is_open()
inFile.open("bowling.txt"); if(!inFile.is_open()) { exit(EXIT_FAILURE);(函数exit()终止程序,在cstdlib中定义) }
注意:文件必须存在,通常除非在输入的文件名中包含路径,否则程序将在可执行文件所属的文件夹中查找。
读取文件时有几点需要检查:首先,程序读取不应超过EOF,如果在最后一次读取数据时遇到EOF,方法eof()将返回true;其次,程序可能遇到类型不匹配的情况,方法fail()将返回true(如果遇到EOF,fail()也会返回true);最后,可能出现意外的问题,文件损坏或硬件故障,方法bad()将返回true;如果不分别检查这些情况,一种更简单的方法是使用good()方法,在没有发生任何错误时返回true
while(inFile.good()) { ...... }
26.内联函数:编译器将使用相应的函数代码来替换函数调用,对于内联代码,程序无需跳到另一个位置执行代码再跳回来,因此内联函数运行速度比常规函数快,但代价是占用更多内存。
使用这项特性必须在函数声明和函数定义前加上关键字inline,如:
inline double square(double x) {return x*x;}
注意到整个函数定义都放在一行中,但并非一定要这么做。然而如果函数定义占用多行,则将其作为内联函数就不太合适。
27.引用变量(引用是已定义变量的别名),引用变量的主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是副本。
c++给&赋予了另一个定义,如:将rodents作为rats变量的别名,且允许将rodents和rats互换,他们指向相同的值和内存单元,
int rats; int & rodents=rats;
引用更接近const指针,必须在声明时初始化
int & rodents=rats; //int *const pr=&rats;
按引用传递
void swap(int &a ,int &b); ... swap(a,b);
28.临时变量:函数调用时如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来储存值。
29.函数返回引用的优点:效率更高
返回引用的注意的问题:避免返回一个指向临时变量的引用
int &clone(int &ft) { int a; a=ft; return a; }//是错误的
或者使用new来分配新的存储空间
int &clone(int &ft) { int *a=new int; *a=ft; return *a; }
30.将引用用于类对象,可以通过使用引用,让函数将类string,ostream,istream,ofstream,ifstream等类的对象作为参数
string version(const string & s1 , const string &s2); ...... string version(const string & s1 , const string &s2) { string temp; temp=s1+s2+s1; return temp; }
31.默认参数,指的是当函数调用中省略了实参时自动使用的一个值
设置默认值必须通过函数原型,(只有函数原型指定了默认值,函数定义与没有默认参数时完全相同)
char * left (const char *str ,int n = 1); //该函数返回一个新的字符串,并使原始字符串保持不变,且默认值是1
对于上述函数,如果省略参数n,它的值将为1,否则传递的值将覆盖1;
实参按从左到右的顺序依次被赋予给相应的参数,而不能跳过任何参数,因此对于带参数列表的函数,必须从右向左添加默认值,
int fun(int n , int a = 1 ,int b = 2);
33.左值与右值
左值 (lvalue, locator value) 表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
右值 (rvalue) 则使用排除法来定义。一个表达式不是 左值 就是 右值 。 那么,右值是一个 不 表示内存中某个可识别位置的对象的表达式。
int x=1; int y=2; //x,y为左值,x+y为右值
34.函数重载,指的是可以有多个同名的函数
函数重载的关键是函数的参数列表———即函数特征标,使用被重载的函数时,需要在函数调用时使用正确的参数类型
void print(char * str , int wide); void print (double d); int printf(char *str); ...... printf(4.43);
若不与任何类型匹配,则c++会尝试使用标准类型转换强制进行匹配,但存在多种转换方式,c++会将其视为错误!
编译器在检查函数特征标时,将把类型引用和类型本身视作同一个特征标!
函数重载的返回类型可以不同,但特征标必须不同!
!!!仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载!!!
35.函数模板,
将同一种算法用于不同类型的函数时,请使用模板;
模板并不创建任何函数,只是告诉编译器如何定义函数;
template <typename T> void Swap (T & a , T & b) { T temp = a; a = b; b = temp; }//关键字template,typename是必需的,T是类型名可任意选择
函数模板不能缩短可执行程序,最终的代码不包含任何模板,而只包含了为程序程序生成的实际函数。
重载的模板,
template <typename T> void Swap (T & a , T & b); template <typename T> void Swap(T * a , T * b , int); //并非所有的模板参数都必须是模板参数类型
显式具体化,
具体化优于常规模板,而非模板函数优先于具体化和常规模板;
//下面是用于交换job结构的非模板函数,模板函数和具体化的原型 void swap ( job & a ,job & b); template <typename T> void swap ( T & a , T & b); template <>void swap <job> (job & a , job & b);
实例化和具体化,
隐式实例化,显式实例化和显式具体化统称为具体化,他们表示的都是使用具体类型的函数定义,而不是通用描述。
template <typename T> void swap(T & a , T & b); //隐式实例化 函数调用swap(i,j)导致编译器生成swap()的一个实例 ,模板并非函数定义,但使用int的模板实例是函数定义 template void swap <char> (char & a , char & b); //显式实例化,使用swap()模板生成int类型的函数定义 template <>void swap <int>(int & a , int & b); //显示具体化,不要使用swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义
注意:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错!!
若模板与函数调用Add(x,m)不匹配(x为int型,m为double型),可使用Add<double>(x,m)强制为double类型实例化;
内存模型和名称空间
36.单独编译
c++鼓励程序员将组件函数放在独立的文件中,
头文件:包含结构声明和使用这些结构声明的函数的原型。
源代码文件:包含与结构有关的函数的代码。
源代码文件:包含调用与结构相关的函数的代码。
头文件包含了用户定义类型的定义,另一个文件包含操纵用户定义的类型的函数的代码。
头文件常包含的内容:函数原型,使用#define或const定义的符号常量,结构声明,类声明,模板声明,内联函数(不要将函数定义或变量声明放到头文件)。
//头文件
#ifndef COORDIN_H_ #define COORDIN_H_ struct polar { double distance ; double angle ; }; struct rect { double x ; double y ; }; polar rect_to_polar(rect xypos) ; void show_polar(polar dapos) ; #endif //在一个文件中只能将一个头文件包含一次, //仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句
//源文件1 #include <iostream> #include "coordin.h" using namespace std ; int main () { rect rplace ; polar pplace ; while (cin>>rplace.x>>rplace.y) { pplace = rect_to_polar (rplace) ; show_polar (pplace) ; } return 0; }
//源文件2 #include <iostream> #include <cmath> #include "coordin.h" polar rect_to_polar (rect xypos) { using namespace std; ....... } void show_polar (polar dapos) { using namespace std; ...... }
37.存储持续性,作用域和链接性
作用域描述了名称在文件(翻译单元)的多大范围内可见;
链接性描述了名称如何在不同单元间共享;
自动储存持续性,自动变量只在包含他们的函数或代码块中可见;
静态持续变量:外部链接性(可在其他文件中访问),内部链接性(只能在当前文件中访问),无链接性(只能在当前函数或代码块中访问),这些变量在整个程序执行期间一直存在,如果没有显式地初始化静态变量,编译器把他们设置为0;
创建链接性为外部的静态持续变量,必须在代码块的外面声明它;创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并且使用static限定符;创建没有链接性的静态持续变量,必须在代码块内声明它,并且使用static限定符;
... int global = 1000 ;//外部静态持续变量 statci int one_ = 50 ;//内部静态持续变量 int main () { ... } void fun() { static int count = 10 ;//无链接性静态持续变量 }
静态持续性,外部链接性
单定义规则:变量只能有一次定义,定义声明(给变量分配储存空间)和引用声明(不分配已有空间),引用声明使用关键字extern,且不进行初始化,否则声明为定义,导致分配空间;
如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它;
//file_1 int rect = 10 ; ... //file_2 extern int rect ; ...
作用域解析运算符(::),放在变量名前,该运算符表示使用变量的全局版本;
静态持续性,内部链接性
//file_1 int error = 100 ; //file_2 static int error = 100 ;
以上没有违反单定义规则,且在file_2中静态变量将隐藏常规外部变量;
静态储存持续性,无链接性
将导致局部变量的储存持续性为静态的,在函数多次调用之间,静态局部变量的值将保持不变;
说明符与限定符
储存说明符:auto,register,static,extern,mutable;
cv-限定符:const,volatile(关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化,目的是改善编译器的优化能力,将变量声明为volatile相当于告诉编译器,不要进行这种优化);
mutable:可以用它来指出,即使结构(或类)变量为const,其某个成员也可以被修改;
struct data { char name [30]; mutable int access; } const data veep; veep.access++;//可被修改
再谈const:在默认情况下全局变量的链接性为外部,但const全局变量的链接性为内部的(就像使用了static说明符一样),原因是,假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件,所有的源文件将包含类似(const int num = 10;)的定义,如果全局const声明的链接性为外部的,将违反单定义规则。如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性,且在所有使用该常量的文件使用extern来声明它,且只有一个文件可对其进行初始化;
函数和链接性
c++不允许在一个函数内定义另外一个函数,因此所有函数的储存持续性都自动为静态的,在默认情况下,函数的链接性为外部的,还可以使用关键字static将函数的链接性设置为内部的(必须在原型和函数定义中使用关键字),在定义静态函数的文件中,静态函数将覆盖外部定义,单定义规则也适用于非内联函数;
语言链接性
在C语言中,一个名称只对应一个函数,C语言编译器可能将spiif这样的函数名翻译为_spiif,这种方法被称为C语言链接性;
在c++中,同一个名称可能对应多个函数,这些函数将翻译为不同的符号名称,因此c++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称,这种方法被称为c++链接性;
如果要在c++程序中使用C库中预编译的函数,可以用函数原型来指出要使用的约定;
extern "C" void spiif (int);
储存方案和动态分配
动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制;
若有int * p = new int [20];当包含该声明的语句块执行完毕时,p指针将会消失,若将p的链接性声明为外部的,在另一个文件使用extern关键字声明该指针后可使用;
int *p = new int; .... //file_2 extern int *p;
定位new运算符,通常new负责在堆中找到一个足以满足要求的内存块,但定位new运算符让程序员能够指定要使用的地方,首先必须包含头文件new,
#include <new> struct chaff { int a; char n; }; char buffer [50]; int main() { chaff *p1=new chaff;//在堆中分配空间给结构chaff int *p2=new (buffer) int [10];//在buffer中分配空间给一个包含10个元素的int数组 }
定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块;
delete只能用于指向常规new运算符的分配的堆内存的指针,也就是buffer不在delete管辖范围,delete [ ] p2将会出错;
定位new运算符的工作原理只是返回传递给他的地址,并将其强制转换为void*,以便能够赋给任何指针类型;
38.名称空间
传统的c++名称空间
声明区域:是可以在其中进行声明的区域。在函数外声明全局变量,其声明区域为其声明所在的文件 ;对于在函数声明的变量,其声明区域为其声明所在的代码块。
潜在作用域:变量的潜在作用域从声明开始,到其声明区域的结尾;(变量在其潜在作用域的任何位置都是可见的,例如它可能被另一个嵌套声明区域中的同名变量隐藏)
每个声明区域都可以定义声明名称,这些名称独立于在其他声明区域中声明的名称;
新的名称空间特性
通过定义一种新的声明区域来创建命名的名称空间,名称空间可以是全局的,也可以是位于另一个名称空间中,但不能位于代码块中。在默认情况下,在名称空间中声明的名称的链接性为外部的。
名称空间是开放的,可以把名称加入到已有的名称空间中
namespace op{ int a; } namespace op{ int b; }//将b添加到op名称空间中
通过作用域解析符::使用名称空间来限定该名称
op::a=10;
using声明和using编译指令
using声明使特定的标识符可用,using编译指令使整个名称空间可用。
#include <iostream> using namespace std; namespace tr{ int a; } int a; int main() { using namespace tr; int a; cin>>a>>::a>>tr::a;//a是局部声明的,::a是全局的那个,tr::a是tr名称空间的那个,局部声明的a将隐藏tr::a和全局a return 0; }
可以将名称空间进行嵌套,也可以在名称空间中使用using声明和using编译指令。
可以给名称空间创建别名
namespace a=my_a_space;
未命名的名称空间:不能在未命名名称空间所属文件之外的其他文件中使用该名称空间的名称,这提供了链接性为内部的静态变量的替代品。
名称空间示例
//name.h namespace pers { int a; void fun(); } //name1.cpp #include <iostream> #include "name.h" namespace pers { void fun() { ...... } } //name2.cpp #include <iostream> #include "name.h" int main() { using pers::a; std::cin>>a; pers::fun(); return 0; }
对象和类
39.面向对象编程OOP
采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需要的操作。完成对接口的描述后,需要确定如何实现接口和数据储存。最后,使用新的设计方案创建出程序。
40.抽象和类
类是一种将抽象转换为用户定义类型的c++工具,它将数据表示和操纵数据的方法组合成一个整洁的包;
类声明:以数据成员的方式描述数据部分,以成员函数的(被称为方法)的方式描述公有接口。
类方法定义:描述如何定义类成员函数。
c++程序员将接口(类定义)放在头文件,并将实现(类方法的代码)放在源代码文件中。
//将类名首字母大写
//stock.h
#ifndef STOCK00_H_ #define STOCK00_H_ #include <string> class Stock//关键字class指出这些代码定义了类设计,该声明让我们能够声明Stock类型的变量——称为对象或实例 { private ://private可用可不用 std::string com; double share; void set() {...}//成员函数可以就地定义,也可以用原型表示 public : void acquire(std::string & co , double n ); void show(); }; #endif
访问控制,
使用类对象的程序可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此,公有成员函数是程序与对象的私有成员之间的桥梁,提供了对象和程序之间的接口。
防止程序直接访问数据被称为数据隐藏。
类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件,将实现细节放在一起并将它们与抽象分开被称为封装。(数据隐藏是一种封装,将实现的细节隐藏在私有部分中也是一种封装,将类函数定义和类声明放在不同的文件中也是一种封装)
原则是将实现细节从接口设计中分离出来,在修改细节时,则无需修改程序接口,这使程序维护起来更容易。
控制对成员的访问(公有还是私有),
隐藏数据是OOP主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分。通常程序员使用私有成员函数来处理不属于公有接口的实现细节。
实现类成员函数,
定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;类方法可以访问private组件。
void Stock ::show()
其他成员函数不必使用作用域解析运算符,就可以使用show()方法,且方法可以访问类的私有成员。
定义于类声明中的函数将自动成为内联函数,也可以在类声明外定义成员函数,在类实现部分中定义函数时使用inline限定符即可,内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对所有文件可用,将内联定义放在定义类的头文件中。
inline void Stock::set() { ... }
方法使用哪个对象
Stock kate; Stock joe; //创建两个类对象,kate和joe kare.show(); joe.show(); //调用kate对象的show()成员,意味着把share解释成kate.share
所创建的每个新对象都有自己的储存空间,但同一个类的所有对象共享同一组类方法,在OOP中,调用成员函数称为发送消息消息,将同样的消息发给多个不同的对象将调用同一个方法。
使用类
要创建类对象,可以声名类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。
(客户/服务器模型:类声明包括类方法构成了服务器,它是程序唯一可以使用的资源,服务器的责任是确保服务器根据该接口可靠并准确地执行)
修改实现
使用方法set()可避免科学计数法
std::cout.set(std::ios_base,std::ios_base::floatfield);
或者用precision()使用定点标记法
std::cout.precision(3);//显示三位小数
上述修改将一直有效,直到再次修改,这可能影响客户程序的后续输出,因此可重置格式信息,使其恢复被自己调用前的状态,而使用返回的值可以实现
std::streamsize prec=std::cout.precision(3); ... std::cout.precision(prec); ... std::ios_base::fmtflags orig=std::cout.setf(std::ios_base::fixed); ... std::cout.setf(orig,std::ios_base::floatfield);
41.类的构造函数和析构函数
c++提供了一个特殊的成员函数——类构造函数,专门用于 构造新对象,将值赋给它们的数据成员。
声明和定义构造函数
原型位于类声明的公有部分,注意没有返回类型
Stock::Stock(const string & co , long n , double pr) { company=co; shares=n; share_val=pr; set_tot(); } ...
程序声明对象时,将自动调用构造函数
(不能将类成员名称用作构造函数的参数名,将会造成混乱a=a的混乱,可以对数据成员名使用m_前缀或使用_后缀)
使用构造函数
显式地调用构造函数和隐式地调用是等价的,但隐式格式更紧凑
Stock garment=Stock("world",90,3.6);//显式 Stock garment("world",90,3.6);//隐式
每次创建类对象(包括new动态分配内存)时,都将使用类构造函数
Stock *pstock=new Stock ("world",90,3.6);
在这种情况下,对象的地址被赋给pstock指针,但对象没有名称。
一般来说,使用对象来调用方法,但无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。
默认构造函数
在未提供显式初始值时,用来创建对象的构造函数。
当且仅当没有定义任何构造函数时编译器才会提供默认构造函数,如果提供了非默认构造函数,而没有提供默认构造函数,如下将会出错
Stock stock1;
通过函数重载来定义另一个构造函数——一个没有从参数的构造函数
Stock::Stock() { } //通常应初始化所有对象,以确保所有成员一开始就有已知的合理值 Stock::Stock() { company="no name"; shares=0; }
创建默认构造函数后就可以声明对象变量,而不对它们进行显式初始化
Stock first; Stock first=Stock(); Stock *prelief=new Stock;
析构函数
析构函数完成清理工作,如果构造函数使用new来分配内存,则析构函数将使用delete来释放内存;如果没有使用new,析构函数实际上没有任务,这时候让编译器生成一个什么都不做的隐式析构函数即可
析构函数名称为在类名前加上~,析构函数也可以没有返回值和声明类型,但析构函数没有参数
Stock::~Stock() { } //没new时,也可以不定义让编译器来 class aaa { public: aaa(){p = new char[1024];} //申请堆内存 ~aaa(){cout<<"deconstructor"<<endl; delete []p;} void disp(){cout<<"disp"<<endl;} private: char *p; }; void main() { aaa a; a.~aaa(); a.disp(); } //有new时,使用delete释放内存 //这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);第二次调用析构函数,再次释放堆内存(此时报异常),然后释放栈内存,对象销毁
通常不应在代码中显式地调用析构函数。若创建的是静态储存类对象,析构函数将在程序结束时自动被调用;若创建的是自动储存类对象,则析构函数将在程序执行完代码块时(对象是在其中定义的)自动被调用;若是通过new创建的,当使用delete来释放内存时将会被自动调用。
在c++11中可以将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容并用大括号括起
Stock hot={"efsujh",100,2.5}; Stock jock={"unce"};//第二,三个参数将为默认值0 Stock temp {};//与默认构造函数匹配
const成员函数
const Stock land={"efioh"}; land.show();//将会出错,编译器将拒绝第二行
将show()声明和定义的开头改为。只要类方法不修改调用对象,最好将其声明为const。
void show() const; void Stock::show() const;
42.this 指针
假设要比较Stock对象stock1和stock2
const Stock & Stock::compare(const Stock & s)const { if(s.total>total) return s; else return *this;//此处stock1没有别名,则要使用this指针 }
//返回类型为引用意味着返回的是调用对象本身,而不是副本
每个成员函数都有一个this指针,this指针指向调用对象。
43.对象数组
Stock mystuff[3];//使用默认构造函数 Stock stocks[3]={ Stock("ehu"),//使用构造函数 Stock(),//使用默认构造函数 };//剩下的使用默认构造函数
注意:有构造函数就必须要有默认构造函数
44.类作用域
在类中定义的名称的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的
作用域为类的常量
class Bakery { private: const int Months=12; double costs[Months];//False static const int Months=12; double costs[Months];//True }
//这将创建一个名为Months的常量,该常量与其他静态变量储存在一起,而不是储存在对象中,因此只有一个Months变量,被Bakery对象共享
也可以enum {Months=12};
作用域内枚举(c++11)
enum egg{A,B,C}; enum gge{A,B,C};//False,处于相同的作用域内将会发生冲突 enum class egg{A,B,C}; enum class gge{A,B,C}; egg p=egg::A; gge q=gge::A;//True
45.抽象数据类型(ADT)
我们知道栈储存了多个数据项,其次栈(可创建空栈,可压入,可弹出,可查看栈是否填满,可查看栈是否为空),将上述描述转换为一个类声明,其中公有成员提供了栈操作的接口,而私有数据成员负责储存栈数据。类概念非常适合于ADT方法。
若不用特定的类型来定义栈,而是使用typedef,只需修改typedef语句即可更换栈的类型。
使用类
45.运算符重载
Time Time::Sum(const Time & t)const { Time sum; ... return sum; } ... Time total; Time coding(2.3); Time ex(4.1); total=coding.Sum(ex);
使用运算符重载后
Time Time::operator+(const Time & t)const { Time sum; ... return sum; } ... Time total; Time coding(2.3); Time ex(4.1); total=coding+ex; //等价于total=coding.operator+(ex);
重载限制
重载后的运算符必须至少有一个操作数是用户定义的;使用运算符时不能违反运算符原来的句法规则;不能创建新运算符;不能重载以下运算符;
sizeof . .* :: ?: typeid const_cast dynamic_cast reinterpret_cast static_cast
= () [] -> //只能通过成员函数重载
46.友元:友元函数,友元类,友元成员函数
通过让函数成为类的友元,可以赋予该函数与类成员函数相同的访问权限。
对于
Time Time::operator*(double mult)const { ... }
A = B * 2.57将被转换为A = B.operator*(2.57),但A = 2.57 * B将不行,因为左侧的操作数是调用对象但2.57不是对象。
可以使用非成员函数解决,它不是由对象调用的,它使用的所有值都是显式参数,但非成员函数不能直接访问类的私有数据,但友元函数可以
创建友元
在函数原型放在类声明中,并在原型声明前加上关键字friend
friend Time operator*(double m ,const Time & t);
虽然operator*函数是在类声明中声明的,但它不是成员函数,因此不能使用成员函数运算符来调用,但它与成员函数访问权限相同。
这时刚刚的A = 2.57 * B可以转换为A = operator*(2.57,B)。
重载<<运算符
通过Time类声明来让Time类知道如何使用cout
void operator <<(ostream & os , const Time & t ) { os<<t.hours<<"hours"; } ... Time trip; cout<<trip;//调用cout<<trip应使用cout对象本身,因此该函数按引用来传递该对象
但这种实现不允许像通常那样通常那样将重新定义的<<运算符与cout一起使用,需要修改operator<<()函数,让它返回ostream对象的引用即可
ostream & operator <<(ostream & os , const Time & t ) { os<<t.hours<<"hours"; return os; }
47.类的自动类型转换和强制类型转换
Stonewt(double lbs); Stonewt myCat; myCat=19.6; //程序将使用构造函数Stonewy(double)来创建一个临时的Stonewy对象,将19.6作为初始化值,随后采用逐成员赋值方式将该临时对象的内容复制到myCat中,这一过程称为隐式转换
只有接受一个参数的构造函数或其他参数提供默认值的构造函数才能作为转换函数。
关键字explicit用于关闭这种特性
explicit Stonewt(double lbs); Stonewt myCat; myCat=19.6;//错误 myCat=Stonewt(19.6);//仍允许显式转换
函数原型化提供的参数匹配过程,=允许使用Stonewt(double)构造函数来转换其他数值类型
Stonewt jumb(7000); jumb=7300; //都将int转换为double,然后使用Stonewt(double)构造函数,前提是转换不存在二义性,如果还定义了Stonewt(long)编译器将拒绝这些语句
转换函数
构造函数只用于从某种类型到类类型的转换,要进行相反的转换要使用转换函数。
设要转换的类型为typename,则
operator typename();
...
Stonewt wolfe(19.6);
typename star=(typename)wolfe;
且转换函数必须是类方法,转换函数不能指定返回类型,转换函数不能有参数
Stonewt ::operator int()const { return int(pounds+0.5);//pounds为类的一个私有成员 } //int转换将待转换的值四舍五入为最接近的整数,而不是去掉小数部分 Stonewt ppins(9,2.8); std::cout<<int(ppins); int i_wt=ppins;
类和动态内存分配
48.动态内存和类
注意:不能在类声明中初始化静态成员变量(const例外),可以在类声明外使用单独的语句来初始化,因为静态类成员是单独存在的,而不是对象的组成部分。
StringBad::StringBad(const char*s) { len=std::strlen(s); str=new char[len+1];//此处str类型为char* } ... StringBad::~StringBad() { delete [] str; }
字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。
StringBad sailer=sports;//sailer和sports都是StringBad对象 //等价于 StringBad sailer=StringBad(sports); //函数原型为StringBad(const StringBad &)
当使用一个对象来初始化另一个对象,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)
复制构造函数
复制构造函数用于一个对象复制到新创建的对象中,他用于初始化过程中(包括按值传递参数),而不是常规的赋值过程。
StringBad ditto(motto); StringBad metoo=motto; StringBad also=StringBad(motto); StringBad *pStringBad=new StringBad(motto); //以上四种声明都将调用复制构造函数
默认的复制构造函数逐个复制非静态成员,复制的是成员的值。
/*我们使用析构函数(进行-1)和构造函数(进行+1)进行计数,但是默认的复制构造函数不说明其行为,因此它不指出创建过程,也不增加计数器的值。所以在初始化函数形参和新对象的初始化时,只减1不加1*/ //解决方法是提供一个对计数进行更新的显式复制构造函数 StringBad ::StringBad(const String& s) { num++; ... }
隐式复制构造函数是按值复制的,其相当于
sailor.str=sports.str; /*这里复制的不是字符串而是一个指向字符串的指针,此时得到俩个指向同一个字符串的指针,但是析构函数释放sailor的str指针指向的内存后,再释放sports的str指针将会出问题*/
解决类设计中的这种问题的方法是进行深度复制,让每个对象都有自己的数据(这里是字符串),而不是引用另一个对象的数据
StringBad::StringBad(const StringBad & st) { num++; len=st.len; str=new char [len+1];
std::strcpy(str,st.str); }
必须定义复制构造函数的原因在于,一些类成员使用new初始化的,指向数据的指针,而不是数据本身;
静态类成员函数
这将导致不能通过对象调用静态成员函数,静态成员函数甚至不能使用this指针,如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
static int HowMany() {return num;}//把它定义在String类声明中 ... int count=String::HowMany();
由于静态成员函数不与特定的对象 相关联,因此只能使用静态数据成员。可以使用静态成员函数设置类级标志,以控制某些类的接口的行为。
进一步重载赋值运算符
String name; char temp[40]; cin.getline(temp,40); name=temp; /*首先,程序使用构造函数String (const char*)来创建一个临时String变量;然后使用String &String::operator=(const String&)函数将临时对象复制到name对象中;最后调用析构函数~String删除临时对象*/
为了提高处理效率,最简单的方法是重载赋值运算符,使之能够直接使用常规字符串,这样就不用创建和删除临时对象了
String & String::operator=(const char *) { delete [] str; len=std::strlen(s); str=new char[len+1]; std::strcpy(str,s); return *this; }
一般来说,必须释放str指向的内存,并为新字符分配足够的内存。
49.在构造函数中使用new时应注意的事项
如果在构造函数时中使用new来初始化指针成员,则应在析构函数中使用delete;
new和delete必须相互兼容,new对应delete,new[]对应[]delete;
如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带,因为只有一个析构函数;
应定义一个复制构造函数,通过深度复制将以一个对象初始化为另一个对象;
String::String(const String &st) { num++; len=st.len; str=new char[len+1]; std::strcpy(str,st.str); }
应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象;
String &String::operator=(const String & st) { if(this==&st) return *this; delete [] str; len=st.len; str=new char [len+1]; std::strcpy(str,st.str); return*this; }
如果构造函数没有使用new来初始化str,当析构函数使用delete来释放str,结果将是不确定的,可能是有害的
String::String() { str="goodnight"; len=std::strlen(str); } //改为 String::String() { len=0; str=new char[1]; str[0]=‘/n’; }
50.有关返回对象的说明
返回指向const对象的引用
const Vector& Max(const Vector &v1,const Vector &v2) { if(v1.magval>v2.magval) return v1; else return v2; }
返回对象将调用复制构造函数,而返回引用不会;引用指向的对象应在调用函数执行时存在;v1v2都被声明为const引用,因此返回类型必须为const才能匹配。
返回指向非const对象的引用
常见的情形是重载赋值运算符和重载与cout一起使用的<<运算符,前者旨在提高效率,后者必须这么做
返回对象
如果返回的对象是被调用函数中的局部变量,则不应该按引用的方法返回它。通常,被重载的算术运算符属于这一类
Vector force1(50,60); Vector force2(10,70); Vector net; net=force1+force2; Vector Vector::operator+(const Vector & b)const { return Vector(x+b.x,y+b.y); }
//force1和force2在这个过程中应该保持不变,因此返回值不能是指向在调用函数中已经存在对象的引用
返回const对象
担心误用和滥用时,将返回类型声明为const Vector。
总之,如果方法或函数要返回局部对象,应返回对象,而不是对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回对象的引用,在这种情况下,应首选引用,因为其效率更高。
关于初始化
Queue::Queue(int qs) { front=NULL; items=0; qsize=qs;//这里front是指针,items是int成员,qsize是const int类型 } //以上实现方法将错误,调用构造函数时,对象将在括号中的代码执行之前被创建,从而不能给qsize赋值 Queue::Queue(int qs):qsize(qs) { front=NULL; items=0; }//True Queue::Queue(int qs)::qsize(qs),front(NULL),items(0) { }//True
对于非静态const数据成员和引用数据成员必须使用列表初始化,因为都只能在创建时被初始化。
对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。
在c++11中允许类内初始化
class Queue { private: front=NULL; items=0; qsize=10; };
再谈定位运算符
char* buffer=new char [BUF];//BUF是常量 JustTest *pc1,*pc2,*pc3;//JustTest 是类名 pc1=new JustTest("nnunedu"); pc2=new (buffer) JustTest; pc3=new (buffer) JustTest("ecnenen");//出问题 delete pc1;//True delete pc2;//错误,不能这么做
delete [] buffer;
创建第二个对象时,定位new运算符使用一个新对象来覆盖用于第一个对象的内存单元,要使用不同的内存单元,要提供两个位于缓冲区的不同地址
pc2=new (buffer) JustTest; pc3=new (buffer+siezof(JustTest))JustTest("euabcu");
但在delete[ ]buffer时,不会为使用定位运算符创建的对象调用析构函数,需要显式地为使用定位运算符创建的对象调用析构函数
pc2->~JustTest();
pc3->~JustTest();
类继承
面向对象编程的主要目的之一是提供可重用的代码。
类继承可以从已有的类派生出新的类,而派生类继承了原有类(基类)的特征,包括方法。还可以在已有类的基础上添加新功能,给类添加数据,修改类的方法。
51.派生一个类
class Rate::public Table { ... }//将Rate类声明为从Table类派生而来,该声明头表面Table是一个公有基类,这被称为公有派生
使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问
派生类对象储存了基类的数据成员,可以使用基类的方法。
派生类需要自己的构造函数,可以根据需要添加额外的数据成员和成员函数。
构造函数必须给新成员和继承的成员提供数据
class Rate:public Table { private: int rating; public: Rate(int r=0,const string&fn="none",bool ht=false); Rate(int r,const Table&tp); }
Rate构造函数不能直接设置继承的成员,必须使用基类的公有方法来访问私有的基类成员。
创建派生类对象时,程序首先创建基类对象,派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数,还应初始化新增的数据成员
Rate:Rate(int r,const string &fn,bool ht):Table(fn,ht) { rating=r;//这段也可以列表初始化,发现没有const在vc上会编译失败,应该是要求为常量,防止变量改变导致数据成员发生改变 } //如果不调用基类构造函数,程序将使用默认的基类构造函数 Rate::Rate(int r,const Table&tp):Table(tp) { rating=r;//同上 }
派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
派生类和基类之间的特殊关系
除了在基类方法不是私有的条件下,派生类对象可以使用基类的方法;基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。
然而,基类指针或引用只能用于调用基类方法。
52.继承:is-a关系
称为is-a-kind-of(是一种)关系,如香蕉是水果。
(has-a关系:如午餐有水果,将水果对象作为午餐类的数据成员。
is-like-a关系:也就是说不采用明喻,如律师像鲨鱼,但并不是鲨鱼。)
53.多态公有继承
若希望同一个方法在派生类和基类中的行为是不同的,即方法的行为取决于调用该方法的对象,这种较复杂的行为称为多态——具有多种形态。
实现多态公有继承的机制有两种:在派生类中重新定义基类的方法和使用虚方法
在成员函数原型前加上关键字virtual,这些方法被称为虚方法。
virtual void withdraw(); virtual ~Brass();
在基类和派生类中的两个相同的原型声明将有2个独立的方法定义,程序将使用对象类型来确定使用哪个版本。
如果没有virtual关键字,程序将根据引用或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
方法在基类被声明为虚的后,它在派生类中将自动成为虚方法,但在派生类声明中使用virtual来指出哪些是虚函数也不失为一种好方法。
基类声明了一个虚析构函数,这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数。
在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法
void Brassplus::ViewAcct()const { Brass::ViewAcct(); ... } //这里Brassplus是Brass的派生类,使用作用域解析运算符来调用基类方法,否则将会陷入不会终止的递归函数
54.静态联编和动态联编
将源代码的函数调用解释为执行特定的函数代码被称为函数联编。在编译过程中进行联编被称为静态联编,程序运行时进行联编被称为动态联编。
指针和引用类型的兼容性
将派生类引用或指针转换为基类引用或指针被称为向上强制转换
Brassplus dilly("icfejicni"); Brass * pb=&dilly; Brass *rb=dilly;
向上强制转换是可传递的,如果从Brassplus派生出Brassplusplus,Brass指针或引用可以引用Brass对象,Brassplus对象或Brassplusplus对象。
相反的过程——将基类指针或引用转换为派生类引用或指针——称为向下强制转换,如果不使用显式类型转换,则向下强制转换是不允许的。
虚成员函数和动态联编
编译器对非虚方法使用静态联编,对虚方法使用动态联编。
对于虚方法
构造函数不能是虚函数;
析构函数应当是虚函数,除非类不用做基类;
友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数;
假设有
class Dewlling { public: virtual void showperks(int a)const; ... } class Hoval:public Dewlling { public: virtual void showperks()const; ... } Hoval trump; trump.showperks();//OK trump.showperks(4);//NO,False
重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。所以:
如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,被称为返回类型协变。
class Dewlling { public: virtual Dellling& building(int n); ... } class Hoval:public Dewlling { public: virtual Hoval& building(int n); ... }
如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。因为如果只定义一个版本,则另外两个版本将会被隐藏。
55.访问控制:protected
在类外只能使用公有类成员来访问protected部分中的类成员。protected作用主要在派生类中表现出来,其余与private无异。
派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
对于成员函数是来说,保护访问控制很有用,它让派生类能够访问公众不能访问的内部数据。
但是有时候最好对类数据成员采用私有访问控制,而不是保护访问控制,同时通过基类方法使派生类能够访问基类数据。(总之,视情况而定吧hh)
56.抽象基类(abstract base class,ABC)
如果从椭圆(ellipse)中派生出圆(circle),椭圆的很多方法和参数对圆来说没有实际意义。
这时可以从ellipse和circle类中抽象出它们的共性,将这些特性放到一个ABC中,然后从该ABC派生出circle和ellipse类。
c++通过使用纯虚函数提供未实现的函数,纯虚函数的结尾处为=0,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。
class BaseEllipse { private: double x; double y; public: ... virtual double Area()const = 0;//纯虚函数
当类声明中包含纯虚函数,则不能创建该类的对象。
总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征。使用常规虚函数来实现这种接口。
ABC理念
设计ABC之前,首先应开发一个模型——指出编程问题所需的类以及它们之间相互关系。
可以将ABC看作是一种必须实施的接口,ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。
57.继承和动态内存分配
如果基类使用动态内存分配,并重新定义赋值和复制构造函数
当派生类不使用new,不用搞东西hh。
当派生类使用new,必须为派生类定义显式析构函数,复制构造函数和赋值运算符。
hasDMA::hasDMA(const haDMA & hs):baseDMA(hs) { style=new char[std::strlen(hs.style)+1); std::strcpy(style,hs.style); }
hasDMA复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据
hasDMA & hasDMA::operator=(const hasDMA &hs) { if(this=&hs) return *this; baseDMA::operator=(hs);//使用函数表示法调用hasDMA的成员函数 delete [ ] style; style =new char[std::strlen(hs.style)+1]; std::strcpy(style,hs); return *this; }
派生类的显式赋值运算符必须负责所有继承的baseDMA基类对象的赋值,可以通过显式调用基类赋值运算符来完成这项工作。
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类元素。
c++中的代码重用
(has-a关系,包含对象成员的类,模板类valarray,私有和保护继承,多重继承,虚基类,创建类模板,使用类模板,模板的具体化)
58.包含对象的类
valarray类简介(头文件valarray)
valarray类被定义为一个模板类,以便能处理不同的数据。
使用valarray来声明一个对象时,需在标识符valarrray后加上一对尖括号,并在其中包含所需的数据类型
valarray<int>q_values; valarray<double>weights;
valarray类的构造函数
valarray<double>v1;//创建长度为0的空数组 valarray<int>v2(8);//指定长度的空数组 valarray<int>v3(10,8);//所有元素被初始化指定值(这里是10)的数组 valarray<double>v4(gpa,4);//用常规数组中的值进行初始化的数组(gpa为一个数组)
valarray的一些类方法
operator[]();//让您能够访问各个元素 size();//返回包含的元素数 sum();//返回所有元素的总和 max()或min()//返回最大或最小的元素
例如学生和姓名成绩,成绩用valarray类表示,姓名用string,关系是学生有成绩和姓名是has-a关系而非is-a关系,通常建立has-a关系的技术是组合(包含),即创建一个包含其他类对象的类
class Student { private: string name; valarray <double>scores; ... };
接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数不提供实现);而使用组合,类可以获得实现,但不能获得接口。
explicit Student(const string& s) :name(s),score() {} //可以用一个参数调用的构造函数用作从参数类型到类类型的转换的隐式转换函数,但这通常不是什么好主意 explicit Student (int n):name ("Nully"),score(n) {} //参数表示的是数组元素个数,所以将一个构造函数用作int到Student的转换函数是没有意义的,所以用explicit关闭隐式转换
c++和约束
c++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据。根本原因是在编译阶段出现错误优于在运行阶段出现错误。
初始化被包含的对象
Student (const char*str,const double*pd,int n) :name(str),scores(pd,n) { } //因为该构造函数初始化的是成员对戏,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。 //当初始化列表中包含多个项目时,初始化顺序为它们被声明的顺序
使用被包含对象的接口
double Student::Average () const { if(scores.size()>0) return scores.sum()/scores().size(); else return 0; } //被包含对象的接口不是公有的,但可以在类方法中使用它
可以通过私有辅助函数,将凌乱的细节放在一个地方,使得友元函数的编码更整洁(用来输出的友元函数)
//私有方法 ostream &Student::arr_out(ostream & os)const { int i; int lim=scores.size(); if(lim>0) { for(int i=0;i>lim;i++) { os<<score[i]<<" "; ..... return os; }
59.私有继承
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,和包含一样:获得实现但不获得接口,所以私有继承也可以用来实现has-a关系。
class Student:private std::string,private std::valarray<double> { public: ... }; //使用private来定义类(实际上,private是默认值,因此省略也将是私有继承),此处Student由两个类派生而来
使用多个基类的继承被称为多重继承。
包含和私有继承的主要区别:
包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。
对于私有继承,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数
Student(const char* str,const double *pd,int n) :std::string(str),ArrayDb(pd,n) {}
使用私有继承时将使用类名和作用域解析运算符来调用方法,而使用包含时将使用对象来调用方法。
访问基类对象
Student类的包含版本实现Name()方法,它返回string对象成员name;使用私有继承时,该string对象没有名称,要访问内部的string对象需要使用强制类型转换。
const string & Student ::Name()const { return (const string &)*this; } //上述方法返回一个引用,该引用指向用于调用该方法的Student对象中继承而来的string对象
原因在于:在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。且由于这个类使用的是多重继承,如果两个类都提供了函数operator<<(),编译器无法确认应转换为哪个基类。
ostream & operator<<(ostream & ,const Student & stu) { os<<"Scores for"<<(const string & )stu; ... } //用类名显式地限定函数名不适合友元函数,,对于以上友元函数,可以显式地转换为基类来调用正确的函数。
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承(都是派生类能做的,而包含类不是派生类)。
保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected
class Student:protected std::string, protected std::valarray<double> {...};
保护继承和私有继承的区别在于:使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中变成了私有方法;使用保护继承时,基类的公有方法在第二代中变成受保护的,因此第三代派生类可以使用它们。
特征 | 公有继承 | 保护继承 | 私有继承 |
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否向上隐式转换 | 是 | 是(但只能在派生类中) | 否 |
隐式想试试转换意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。
使用using重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将称为保护成员或私有成员。假设要让基类的方法在派生类外面可用,
方法之一是定义一个使用该基类方法的派生类方法
double Student ::sum()const { return std::valarray<double>::sun(); }
方法之二是使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员
class Student:private std::string,private std::valarray<double> { ... public: using std::valarray<double>::min; using std::valarray<double>::max; ... }; //上述using声明使得min和max像Student的公有成员一样使用
注意,using声明只使用成员名——没有圆括号,函数特征标和返回类型。
using声明只适应于继承,而不适用于包含。
60.多重继承
class SingingWaiter:public Waiter,public Singer{...}; //这里Waiter和Singer是Worker的派生类 //此时SingingWaiter将会包含两个Worker组件,但是两个相同组件在这里似乎没什么用,一个人只对应一个名字
c++引入了新技术——虚基类,使MI成为可能。
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
class Singer:virtual public Worker{...}; class Waiter:public virtual Worker{...}; class SingingWaiter:public Singer,public Waiter{...}; //virtual和public的次序无关紧要
现在,SiingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的SInger和Waiter对象共享一个Worker对象。
SingingWaiter(const Worker & wk,int p=0,int v=Singer::other) :Waiter(wk,p),Singer(wk,v){} //按道理Waiter和Singer将会调用它们的构造函数并在它们的构造函数中调用Worker构造函数,然而这里并不行
c++在基类是虚的时,禁止信息通过中间类自动传递给基类,所以wk参数中的信息不会传递给子对象Worker,并且编译器会使用Worker的默认构造函数,若不希望
SingingWaiter(const Worker & wk,int p=0,int v=Singer::other) :Worker(wk),Waiter(wk,p),Singer(wk,v){} //则需要显式地调用所需的构造函数
对于虚基类,这种做法是合法的且必须这样做;但对于非虚基类,则是非法的。
哪个方法
对于单继承,如果没有重新定义show(),则将使用最近祖先中的定义,而在多重继承中,每个直接祖先都有一个show(),这使得调用是二义性的。
可以使用作用域解析运算符来澄清编程者的意图
SingingWaiter nhere("yyubn",2005,6); nhere.Singer::show();
更好的方法是在SingingWaiter中重新定义show(),并指出要使用哪个show()
void SingingWaiter::show() { Singer::show(); }
但是如果SInger或Waiter的show()是调用Worker的show来实现的,那这种递增的方式对SIngingWaiter示例无效。如
void SingingWwaiter::show() { Singer::show(); } //因为它忽略了waiter组件
解决方法之一是使用模块化方式,而不是递增方式,即提供一个只显示Worker组件的方法,一个只显示Waiter或Singer组件的方法,然后在SIngingWaiter::show()中组合
void Worker::Data()const { cout<<........... } void Singer::Data()const { cout<<....... } void Waiter::Data()const { cout<<....... } void SingingWaiter::Data()const { Singer::Data(); Waiter::Data(); } void SingingWaiter::show()const { cout<<.... Worker::Data(); Data(); }
总之,在祖先相同时,使用MI必须引用虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。
61.类模板
模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或函数。
定义类模板
template <class Type> class Stack { ... }; //Type可以换,但通常使用T或Type //class可以换为typename
一些改变
int items [MAX]; //改为 Type items [MAX];
每个函数头都将以相同的模板声明打头
bool Stack::push(const int & items) { ... } //改为
template <class Type>
bool Stack<Type>::push(const int & items) { ... } //注意类限定符也改
由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
使用类模板
Stack<int>ken; Stack<string>col;
上面的模板创建了两个栈,一个用于储存int,另一个用于储存string对象。
template<class T , int n>
上面的参数(指定特定的类型而不是泛型名)称为非类型或表达式参数。
ArrayTP<double,12>eggs; //定义类时,编译器将使用double替换T,12替换n
表达式参数可以是整型,枚举,引用或指针,但不能是浮点型;而且不能修改参数的值或使用参数的地址(即不能使用n++,&n等表达式)
模板多功能性
模板类可以用作基类,组件类或者其他模板的类型参数。
template <class T> class Array { private: T entry; ... }; template <class Type> class Grow:public Array<Type>{...};//用作基类 template<class Tp> class Stack { Array<Tp>ar;//用作组件类 ... }; ... Array<Stack<int> >asi;//用作其他模板的类型参数 //在c++98中两个> >要用空格分开,c++11则不用
可以递归使用模板
Array<Array<int ,5>,10> st; //st是一个包含10个元素的数组,其中每个元素是一个包含5个int元素的数组
可以包含多个类型参数
template<class T1,class T2> class Pair { private: T1 a; T2 b; .... public: T1& first(); .... }; ... template <class T1,class T2> T1& Pair<T1,T2>::first() { .... }
默认类型模板参数
template<class T1,class T2=int> class Tope {...};//可以为类型参数提供默认值 Tope<double,double>m1;//T1,T2都是double型 Tope<double>m2;//T2默认为int型
虽然可以为类模板类型提供默认值,但不能为函数参数提供默认值。然而,可以为非类型参数提供默认值,对于二者都是如此。
62.模板的具体化
隐式实例化,显式实例化和显式具体化统称为具体化。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
隐式实例化
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。(编译器在需要对象之前不会生成类的隐式实例化)
ArrayTP<int , 100>stuff;
显式实例化
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。(声明必须位于模板定义所在的名称空间)
template class ArrayTP<string,100>; //将ArrayTP<string,100>声明为一个类
虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。
显式具体化
显式具体化是特定类型(用于替换模板中泛型)的定义。设有以下用于处理排序的类
template <Typename T> class Sort { ... }; //用T::operate>()方法,对数字管用,但对字符串不管用
这时可以提供一个显式模板具体化,这将采用为具体类型定义的模板而不是泛型定义的模板。(当具体化模板和通用模板都与实例化匹配时,编译器将使用具体化版本)
格式为
template<>class Classname<special_Typename>{...};
template <>class Sort<const char*> { ... };
部分具体化
部分具体化,即部分限制模板的通用性。
关键字template后的<>中声明的是没有被具体化的类型参数。
也可以通过指针提供特殊版本来部分具体化现有的模板
template<class T1,class T2,class T3>class Trio{...}; template<class T1,class T2>class Trio<T1,T2,T2>{...}; template<class T1>class Trio<T1,T1*,T1*>{...}; Trio<int,short,char*>t1;//使用第一个通用模板 Trio<int,short>t2;//使用第二个部分具体化模板 Trio<char,char*,char*>t3;//使用第三个部分具体化模板
成员模板——一个模板类将另一个模板类和模板函数作为其成员。
将模板用作参数——用于实现STL。
63.模板类和友元:非模板友元;约束模板友元,即友元的类型取决于类被实例化时的类型;非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元。
模板类的非模板友元函数
template <class T> class HasFriend { ... public: friend void counts(); friend void report(HasFriend<T>&);//要提供模板类参数,必须指明具体化 };
report()本身并不是模板函数,而只是使用一个模板作参数,这意味着必须要为要使用的友元定义显示具体化
void report(HasFriend<int>&){...}; void report(HasFriend<double>&){...};
模板类的约束模板友元函数
要友元函数本身成为模板,要使类的每一个具体化都获得与友元匹配具体化。
首先,在类定义前面声明每个模板函数
template <Typename T>void counts(); template <Typename T>void reports(T&);
然后在函数中再次将模板声明为友元
template <Typename TT> class HasFriend { ... friend void counts<TT>(); friend void report<>(HasFriend<TT>&);//<>指出这是模板具体化,对于report<>可以为空,因为可以根据参数推断出模板类型参数 };
而且每种T类型都有自己的友元函数。
模板类的非约束模板友元函数
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。
template <Typename T> class ManyFriend { ... template <Typename C,Typename D>friend void show(C&,D&); //与下面的具体化匹配 void show<ManyFriend<int>&,ManyFriend<int>&> (ManyFriend<int>&C,ManyFriend<int>&D); };
template <Typename C,Typename D>friend void show(C& c,D& d)
{
cout<<c.item<<","<<d.item;
} //它是所有ManyFriend具体化的友元,能访问所有具体化的item成员,但它只访问了ManyFriend<int>对象
模板别名
typedef std::array<double,12>arrd; typedef std::array<int,12>arrt; template <typename T> using arraytype=std::array<T,12>; arraytype<double>gallons;//arraytype<T>表示std::array<T,12>
c++11还允许将using用于非模板,用于非模板时,语法与常规typedef相似,且可读性更强
typedef const char* pc1; using pc1=const char*;
友元,异常和其他
类并非只能拥有友元函数,也可以将类作为友元
64.友元类
友元声明可以位于公有,私有或保护部分,其所在的位置无关紧要。
class Tv { public: friend class Remote; ... };
友元成员函数
仅让特定的类成员成为另一个类的友元,在Tv类声明中将其声明为友元
class Tv { friend void Remote::set_chan(Tv & t,int c); ... };
但要编译器处理这条语句,它必须知道Remote的定义,这意味着将Remote定义放到Tv定义前。Remote对象提到了Tv对象,又要Tv定义位于Remote定义前。为了避开这种循环依赖,使用前向声明。
class Tv; class Remote{...}; class Tv{...};
通过在方法定义中使用inline关键字。仍然可以使其成为内联方法
inline void Remote::set_chan(Tv& t,int c){t.channel=c;}
互为友元(如:交互式?)
class Tv { friend class Remote; public: void buzz(Remote& r); }; class Remote { friend class Tv; public: void volop(Tv& t){t.volup();} }; inline void Tv::buzz(Remote& r) {...}//如不希望是内联的,则应在一个单独的方法定义文件中定义它
共同的友元......
65.嵌套类
在另一个类中声明的类称为嵌套类,对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。
class Queue { class Node { public: Item item; Node* next; Node(const Item & i):item(i),next(0){ } }; ... };
嵌套类和访问权限和访问控制
声明位置 | 包含它的类是否可以使用它 | 从包含它的类派生而来的类是否可以使用它 | 在外部是否可以使用 |
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符来使用 |
class Team { public: class Coach{..}; ... }; Team::Coach Tom;
Queue类对象只能显式地访问Node对象的公有成员,且Node类对外部世界是不可见的。
总之,类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有,保护,私有,友元)将决定程序对嵌套类成员的访问权限。
66.异常
程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。(如零除)
调用abort()——位于头文件cstdlib中
其典型实现是向标准错误流发送消息abnormal program termination(程序异常终止),然后终止程序。
if(a==-b) std::abort();
返回错误码——一种比异常终止更灵活的方法,使用函数返回值来指出问题
bool hmean(double a,double b,double *ans) { if(a==-b) { *ans=DBL_MAX; return false; } else { *ans=2.0*a*b/(a+b); return true; } }
异常机制
由引发异常,使用处理程序捕获异常,使用try块。
double x,y,z; while(std::cin>>x>>y) { try{ z=hmean(x,y); } catch(const char* s)//异常处理程序,catch块 { std::cout<<s<<std::endl; std::cout<<"Enter new nums";
continue; } } double hmean(double a,double b) { if(a==-b) throw"a=-b is not allowed";//命令程序跳转到另一条语句,将字符串传递给catch的参数 return 2.0*a*b/(a+b); }
若没有引发异常则跳过catch。
函数没有try块或没有匹配的处理程序,在默认情况下,程序最终将调用abort()函数。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析