C++笔记博客版(错别字和内容修改):分章
C++ Primer Plus(第六版)笔记
- 浮点运算的速度通常比整型运算慢,
对于标量运算float和double都不了没有明显差别
对于矢量运算double比float慢得多
- 运算符重载(operator overloading):使用相同符号进行多种操作
1.C++内置重载 9/5 int ; 9L/5L long ; 9.0/5.0 double ; 9.0f/5.0f float
2.C++扩展运算符重载
- int guess(3.9832);结果:guess=3; 将浮点float转换为整型int时,采用截取(丢弃小数部分),而不是四舍五入
- 将一个值赋值给取值范围更大的类型通常不会导致什么问题,只是占用的字节更多而已。
- 列表初始化(使用大括号初始化)不允许窄缩(
float-->int
)。 - (long)thorn; long(thron);强制类型转换不会改变thorn变量本身,而是创建一个新的,指定类型的值。
- auto让编译器能够根据初始值的类型推断变量的类型。
- C++的基本类型
- 整数值(内存量及有无符号): bool,char,signed char,unsigned char,short,unsigned short,int,unsigned int,long,unsigned long,(新)long long,unsigned long
- 浮点格式的值:float(32位),double(64位),long double(94~128位)
- 复合类型:数组;字符串:1.字符数组char array 2.string类;结构:struct;共同体:union;枚举:enum;指针:int* ,long*
short months[12];
int yamcosts[3]={20,30.5};
double earning[4]{1.2e4,1.6e4,1.4e4,1.7e4};
float balances[100]{};//初始化全部元素值为0
//字符串
char boss[8]="Bozo"//后面四个元素为"\0"空字符
- using namespace XXX;这是指示
引入名称空间内所有的名称:将XXX名称空间,所有成员变成可见,作用域和using声明一致;例:
using namespace std;
- using XXX;这是声明
引入名称空间或基类作用域内已经被声明的名称:一次只引入一个命名空间成员;
using std::cout;
对象和变量都是用来描述一段内存的。
- 变量更强调的是变量名这个符号的含义,更强调名字与内存的联系,而不必关注这段内存是什么类型,有多少字节长度,只关注这个变量名a对应着某段内存。
- 而对象的描述更强调的是内存的类型而不在乎名字,也就是说,从对象的角度看内存,就需要清除这段内存的字节长度等信息,而不是关注这个对象在代码中是否有一个变量名来引用这段内存。
- struct和class的区别
struct能包含成员函数吗? 能!
struct能继承吗? 能!!
struct能实现多态吗? 能!!!
既然这些它都能实现,那它和class还能有什么区别? 最本质的一个区别就是默认的访问控制,体现在两个方面:默认继承访问权限和默认成员访问权限
- 1)默认的继承访问权限。struct是public的,class是private的。
- 2)struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
- 做个总结,从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
它能够存储不同的数据类型,但只能同时存储其中的一种类型。
这种特性使得当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。
使用场合:1.对内存的使用苛刻,如嵌入式系统编程 2.操作系统数据结构或硬件数据结构
- 提供了一种创建符号常量的方式,这种方式可以替代const。
- 它还允许定义新的类型,但必须按严格的限制进行。
enum spectrum{red,orange,yellow,green,blue,violet,indigo,wltraciolet};//对应整数值0~7(声明定义)
//在不进行强制类型转换的情况下,只能将定义使用的枚举量赋给这种枚举的变量。
spectrum band;//声明定义
band = blue;//初始化(赋值)
//枚举量是整型,可悲提升为int型
int color = blue;
//设置枚举量的值;
enum bits{one=1,two=2,four=4,eight=8};
enum bigstep{first,second=100,third};//first=0,third=101
//枚举的取值范围
bits myflag;
myflag=bits(6);//强制类型转换(整数值),保证bits()输入的参数小茹bits的上限,上限=(2^n-1)>max,max在bits中等于8
指针与C++基本原理 1.编译阶段:编译器将程序组合起来
2.运行阶段:程序正在运行时--》oop强调的是在运行阶段进行决策
- 考虑为数组分配内存的情况,C++采用的方法是:使用关键字new请求正确数量的内存以及使用指针来跟踪新分配内存的位置
*运算符
被称为间接值运算符或叫解除引用运算符(对指针解除引用意味着获得指针指向的值)。
&地址运算符
注意:int * p1,p2;
p1是指针,p2是int变量;对于每个指针变量名,都需要一个*
- 定义与初始化
int h = 5;
int *pt =& h;
//或
int *pt;
pt = &h;
应用*
之前,一定要将指针初始化为一个确定的,适当的地址。就是说一定要初始化,否则*pt
将值会赋给一个未知内存。否者都还没引用,又怎么接触引用呢?
- 要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型。
pt=(int *)0×B8000000;
变量:在编译时分配的有名称的内存。
指针的真正的用武之地在于,在运行阶段分配未命名的内存以及存储值,(C++中使用new运算符来实现)在这种情况下,只能通过指针来访问内存--->所以new的出现都会有指针。
typeName * pointer_name=new typeName;//使用new分配未命名的内存
* pointer_name=1000;//对该未去命名的内存赋值
-
new从被称为**堆(heap)或自由存储区(free store)**的内存区域分配内存。
delete pointer_name;
//释放指针pointer_name指向的内存。释放pointer_name指向的内存,但不会删除pointer_name指针本身。例如,可以将pointer_name重新指向另外一个新分配的内存块。不要创建两个指向同一内存块的指针 -
对于大型数据对象来说,使用new,如数组、字符串、结构。
- 1.静态联编(static binding)
如果通过声明来创建数组,则程序被编译时将为它分配内存空间,不管程序最终是否使用数组,数组都在那里。它占用了内存,所以必须指定数组长度。
- 2.动态联编(dynamic binding)
意味着数组是在程序运行时创建的,这种数组叫作的哦你太数组。
使用new创建动态数组-->Vector模板类是替代品
//创建
int * psome =new int[10];
//释放
delete[] psome;//方括号告诉程序,应释放整个数组。
将整数变量加1后,其值将增加1,
将指针变量加1后,增加的量等于它指向类型的字节数。
- 指针与数组之间的转换
数组:arrayname[i]等价于*(arrayname+i)
指针:pointername[i]等价于*(pointername+i)
因此,很多情况下,可以使用相同的方式使用数组名和指针名
const char *bird ='"wren"
bird的值可以修改,但*bird
值不可以修改。其实应该说是不能使用bird
指针来修改!!!
- 常量指针:const修饰的是“char * bird”,里面的值是不可以改变的。可以使用指针bird访问字符串“wren”但不能修改字符串。
char * const p ="wren";
- 指针常量:const修饰的是指针“p”,指针的值是不能改变的。
运行时创建数组(结构)由于编译时创建数组(结构)
创建一个未命名的inflatable类型,并将其地址赋给一个指针。
inflatable *ps=new inflatble
- 自动存储
- 静态存储
- 动态存储-->有时也叫自由存储空间或堆
- 线程存储(C++11新增-->第9章)
自动存储:自动变量(函数内部定义的常规变量)通常存储在栈中
--->随函数被调用生产,随该函数结束而消亡
--->自动变量是个局部变量,作用域为包含的代码块({...})
静态存储:使变量称为静态
- 1.在函数外面定义它
- 2.在声明变量是使用static关键字
static double free = 5650;
动态存储:使用new和delete(可能导致占用的自由存储去不连续)对数据的生命周期不完全受程序或函数的生存周期不完全受程序或函数的生存时间控制。
如果使用new运算符在自由存储(或堆)上创建变量后,没有调用delete。则即使包含指针的内存由于副作用或规则和对象生命周期的原因而被释放(将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏),在自由存储空间上动态内存分配的变量或结构也将继续存在。
数组名是一个指针
- 要用指向成员运算符
a_y_e trio[3];
trio[0].year=2003;
(trio+1)->year=2004;
//创建指针数组
const a_y_e *arp[3]={&s01,&s02,&s03};
std::cout<<arp[1]->year<<std::endl;
//可创建指向上述收集自的指针:
const a_y_e **ppa =arp;//麻烦
//可以auto,让编译器自动推断
auto ppa=arps;
- 1.模板类vector-->是一种动态数组-->可以在运行时设置长度-->它是使用new创建动态数组的替代品。
- vector类自动通过new和delete来管理内存。
vector<typeName> vt(n_elm);
typeName:类型,vt:对象名,n_elm:个数:整型常量/变量
- 2.模板类array(C++11)-->与数组一样,array对象长度也是固定的,也使用栈(静态内存分配),而不是自由存储去,因此其效率与数组相同,更方便,更安全。
array<int,5>ai;
array<double,4>ad={1.2,2.1,3.4,4.3};//列表初始化
在C++11中,STL中提拱了一个新的容器std::array,该容器在某些程度上替代了之前版本的std::vector的使用,更可以替代之前的自建数组的使用。那针对这三种不同的使用方式,先简单的做个比较:
相同点:
-
三者均可以使用下标运算符对元素进行操作,即vector和array都针对下标运算符[]进行了重载
-
三者在内存的方面都使用连续内存,即在vector和array的底层存储结构均使用数组
不同点:
-
vector属于变长容器,即可以根据数据的插入删除重新构建容器容量;但array和数组属于定长容量。
-
vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。而数组只能通过下标访问,在程序的设计过程中,更容易引发访问 错误。
-
vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种
-
vector和array提供了size和判空的获取机制,而数组只能通过遍历或者通过额外的变量记录数组的size
-
vector和array提供了两个容器对象的内容交换,即swap的机制,而数组对于交换只能通过遍历的方式,逐个元素交换的方式使用
-
array提供了初始化所有成员的方法fill
-
vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成。**但是vector的插入删除效率不高(从中间插入和删除会造成内存块的拷贝),但能进行高效的随机存储,list能高效地进行插入和删除,但随机存取非常没有效率遍历成本高。
-
由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否失效的问题。
基于上面的比较,在使用的过程中,可以将那些vector或者map当成数组使用的方式解放出来,可以直接使用array;也可以将普通使用数组但对自己使用的过程中的安全存在质疑的代码用array解放出来。
函数---C++的编程模块(要提高编程效率,可更深入地学习STL和BOOST C++提供的功能)
- 1.提供函数定义 function definition
- 2.提供函数原型 function prototype
- 3.调用函数 function call
Void functionName(parameterlist)
{
statement(s)
teturn;
}
- parameterlist:指定了传递给函数的参数类型和数量
- void:没有返回值,对于有返回值的函数,必须有返回语句return
- 1.返回值类型有一定的喜爱内置:不能是数组,但可以是其他任何类型---整数,浮点数,指针,甚至可以是结构和对象。
- 2.函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。
原型描述了函数到编译器的接口,它将1.函数返回值类型(如果有的话)以及2.参数的类型和3.数量告诉编译器。(在原型的参数列表中,可以包含变量名,也可以不包含。原型中的变量名相当于占位符,因此不必与函数中的变量名相同)
- 确保:编译器正确处理1,编译器检查2,3
- 用于接收传递值的变量被称为形参(parameter),传递给函数的值被称为实参(argument)。
- 值传递:调用函数时,使用的是实参的副本,而不是原来的数据。
- 在函数中声明的变量(局部变量(自动变量))(包括参数)是该函数私有的,函数调用时:计算机将为这些变量分配内存;函数结束时:计算机将释放这些变量使用的内存。
int sum_arr(int arr[],int n);//arr=arrayname.n=size
int sum_arr(int arr[],int n);//arr=arrayname.n=size
//两者是等价的
- const保护数组(输入数组原数据不能改变)
void show_array(const double ar[],int n);
//声明形参时使用const关键字 - 该声明表明,指针or指向的是常量数据。这意味着不能使用or修改数据。这并不意味着原始数据必须是常量
- 如果该函数要修改数组的值,声明ar时不能使用const
- 1.对于处理数组的C++函数,必须将数组中的
1.数据类型
2.数组的起始位置
3.和数组元素中的数量提交给他
-
传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组位置和数据类型)
-
2.第二种方法:指定元素区间(range) 通过传递两个指针来完成:一个指针表示数组的开头,另外一个指针表示数组的尾部。例子:
int sum_arr(const int *begun,const int *end)
{
const int *pt;
int total=0;
for(pt=begin;pt!=end;pt++)
total=toatl+*pt;
return total;
}
int cookies[ArSize]= {1,2,4,8,16,32,64,128};
int sum=sum_arr(cookies,cookies+ArSize);
假设要将字符串(实际传递的是字符串第一字符的地址)作为参数传递给函数,则表示字符串的方式有三种:
- 1.char数组
- 2.用隐含阔气的字符串常量
- 3.被设置为字符串的地址的char指针。
涉及函数时,结构变量的行为更接近基于基本的单值变量
- 1.按值传递-->如果结构非常大,则复制结构将增加内存要求,且使用的是原始变量的副本
- 2.传递结构的地址,然后使用指针来访问结构的内容
rect rplace;
polar pplace;
void rect_to_polar(const rect*pxy,polar*pda)
{
...
}
rect_to_polar(&rplace,&pplace);
调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;将形参声明为指向polar的指针,即polar*
类型。由于函数不应该修改结构,因此使用了const修饰符,由于形参是指针不是结构,因此应使用姐姐成员运算符(->),而不是成员运算符(.)。
- 3.按引传递用,传指针和传引用效率都高,一般主张是引用传递代码逻辑更加紧凑清晰。
1.包含一个递归调用的递归
void recurs(argumentlist)
{
statement1
if(test)
recurs(arguments)
statement2
}
如果调用5次recurs就会运行5次statement1,运行1次statement2.
2.包含多个递归调用的递归
void recurs(argumentlist)
{
if(test)
return;
statement;
recurs(argumentlist1);
recurs(argumentlist2);
}
3.从1加到n
class Solution
{
public:
int Sum_Solution(int n){
int ans=n;
ans&&(ans+=Sum_Solution(n-1));
return ans;
}
};
//&&就是逻辑与,逻辑与有个短路特点,前面为假,后面不计算。
函数也有地址---存储其机器语言代码的内存的开始地址
-
- 获取函数的地址,只要使用函数名(后面不跟参数)即可。
例如think()是个函数
process(think);//传递的是地址
thought(think());//传递的是函数返回值
//使用
double pam(int);//原始函数声明
double (*pf)(int);//函数指针声明
pf=pam;//使用指针指向pam函数
double x=pam(4);//使用函数名调用pam()
double y=(*pf)(5);//使用指针调用pam()
//也可以这样使用函数指针
double y=pf(5);
-
- 进阶 下面函数原型的特征表和返回类型相同
const double *f1(const double ar[],int n);
const double *f2(const dopuble [],int );
const double *f3(const double *,int );
//声明一个指针可以指向f1,f2,f3
const double * (*p1)(const double *,int );//返回类型相同,函数的特征标相同
//声明并初始化
const double * (*p1)(const double *,int )=f1;
//也可以使用自动类型推断
auto p2=f2;
-
- 使用for循环通过指针依次条用每个函数
例子:声明包含三个函数指针的数组,并初始化
const double * (*pa[3])(const double *,int)={f1,f2,f3};
问:为什么不使用自动类型推断?auto
答:因为自动类型推断只能用于单值初始化,而不能用初始化列表。
但可以声明相同类型的数组 auto pb=pa;
使用:
const double *px=pa[0](av.3);//两种表示法都可以
const double *py=pb[1](av.3);
//创建指向整个数组的指针。由于数组名pa是指向函数指针的指针
auto pc=&pa;//c++11
//等价于
const double * (*(*pd[3]))(const double *,int)=&pa;//C++98
- 除了auto外,其他简化声明的工具,typedef进行简化
点云库里常常用到,如:
typedef pcl::PointNormal PointNT
typedef const double * (*p_fun)(const double *,int );
p_fun p1=f1;
C++11新特性
- 函数内联
- 按引用传递变量
- 默认参数值
- 函数重载(多态)
- 模板函数
c++内联函数-->提高程序运行速度:常规函数与内联函数的区别在于,C++编译器如何将它们组合到程序中
- 常规函数调用过程:
- 执行到函数调用指令程序在函数调用后立即存储该指令地址,并将函数参数复制到堆栈中(为此保留的代码),
- 跳到标记起点内存单元,
- 执行函数代码(也许将返回值放入寄存器中),
- 然后跳回地址被保存的指令处。
来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
- 情况:函数代码执行时间很短---内联调用就可以节省非内联调用的大部分时间(节省时间绝对值并不大)
- 代价:需要占用更多的内存:如果程序在是个不同地方调用一个内联函数,则该函数将包含该函数代码的10个副本
- 使用:
在函数声明前加上关键字inline;
在函数定义前加上关键字inline;
通常的做法是省略原型,将整个定义(即函数头和所有代码),放在本应提供原型的地方。
- 内联函数不能递归
- 如果函数占用多行(假设没有冗长的标识符),将其作为内联函数不太合适.
C语言使用预处理语句#define
来提供宏---内联代码的原始实现
# define SQUARE(X) X*X
- 这不是通过传递参数实现的,而是通过文本替换实现的---X是"参数"的符号标记。所以宏不能按值传递
故有时候会出现错误
c=10;
d=SQUARE(C++);is replaced by d=C++*c++=11X12=122
引用变量-->是复合类型
int & rodents =rats;
其中int &是类型,该声明允许将rats和rodent互换---他们指向相同的值和内存单元。
- 必须在声明引用变量时进行初始化
- 引用更接近const指针(指向const数据的指针),必须在创建时进行初始化,一旦与某个变量关联起来就一直效忠于它。
int & rodents=rats;
//实际上是下述代码的伪装表示
int * const pr=&rats;
//引用rodents扮演的角色与*pr相同。
//*pr值是个地址,且该地址恒等于&rat-->rats的地址
第十八章将讨论如何使用右值引用来实现移动语义(move semantics),以前的引用(使用&声明的引用)现在称为左值引用
-
- 右值引用是对临时对象的一种引用,它是在初始化时完成的,但右值引用不代表引用临时对象后,就不能改变右值引用所引用对象的值,仍然可以初始化后改变临时对象的值
-
- 右值短暂,右值只能绑定到临时对象。所引用对象将要销毁或没有其他用户
-
- 初始化右值引用一定要用一个右值表达式绑定。
例子:
double &&rref=std::sqrt(36.00);//在左值引用中不成立,即使用&来实现也是不允许的
double j=15.0;
double&& jref=2.0*j+18.5;//同样使用左值引用是不能实现的。
引用非常适合用于结构和类(C++用户定义类型)而不是基本的内置类型。
- 声明函数原型,在函数中将指向该结构的引用作为参数:
void set_pc(free_throws & tf);
如果不希望函数修改传入的结构。可使用const;void display(free_throws & tf);
- 返回引用:free_throws &accumlate(free_throws& traget,free_throws& source);为何要返回引用?如果accumlate()返回一个结构,如:dup=accumlate(team,five) 而不是指向结构的引用。这将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,直接把team复制到dup,其效率更高,复制两次和复制一次的区别。
- 应避免返回函数终止时,不在存在的内存单元引用。为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用指向调用函数使用的数据,因此返回引用也将指向这些数据。
free_throws& accumlate(free_throws& traget,free_throws& source)
{
traget.attempts+=source.attempts;
traget.mode+=source.mode;
set_pc(target);
return target;
}
- 另一种方法是用new来分配新的存储空间
const free_throws& clone(&three)
{
free_throws * pt;//创建无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构,在不需要new分配的内存时,应使用delete来释放它们。
//auto_ptr模板以及unique_ptr可帮助程序员自动完成释放
* pt=ft;
return *pt;//实际上返回的是该结构的引用
}
和结构同理
使得能够将特性从一个类传递给另外一个类的语言被称为继承
ostream-->基类 ofstream-->派生类
基类引用可以指向派生类对象,而无需强制类型转换
使用引用参数到主要原因有两个:
(1)程序员能够修改调用函数中的数据对象。
(2)通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么什么时候应该使用引用,什么时候应该使用指针呢?什么时候应该按值传递呢?下面是一些指导原则:
对于使用传递到值而不做修改到函数:
(1)如果数据对象很小,如内置数据类型或小型结构,则按值传递。
(2)如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
(3)如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需要的时间和空间。
(4)如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
(1)如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
(2)如果数据对象是数组,则只能使用指针。
(3)如果数据对象是结构,则使用引用或指针。
(4)如果数据对象是类对象,则使用引用。
当然,这只是一些指导原则,很可能有充分到理由做出其他的选择。例如,对于基本类型,cin使用引用,因此可以使用cin>>n,而不是cin>>&n。
如何设置默认值?**必须通过函数原型
char* left(const char* str,int n=1);
原型声明
定义长这样 char * left(const char* str,int n){...}
对于带参数列表的函数,必须从左向右添加默认值:下面代码错误,int j应该也设默认值
int chico(int n,int m=6,int j);//fault
- 通过默认参数,可以减少要定义的析构函数,方法以及方法重载的数量
-
- 默认参数让你能够使用不同数目的参数调用的同一个函数。
-
- 而函数多态(函数重载)让你能够使用多个同名函数。
-
- 仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应用函数重载
-
- C++使用名称修饰(名称矫正)来跟踪每一个重载函数
未经过修饰:long MyFunction(int,float);
名称修饰(内部转换):?MyFunctionFoo@@YAXH
--->将对参数数目和类型进行编码
-
- 重载:是指允许存在多个同名方法,而这些方法的参数不同(特征标不同)。重载的实现是:编译器根据方法不同的参数表,对同名方法的名称做修饰,对于编译器而言,这些同名方法就成了不同的方法。他们的调用地址在编译器就绑定了。**重载,是在编译阶段便已确定具体的代码,对同名不同参数的方法调用(静态联编)
- C++中,子类中若有同名函数则隐藏父类的同名函数,即子类如果有永明函数则不能继承父类的重载。
-
- 多态:是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据赋给它的不同的子类,动态调用属于子类的方法,这样的方法调用在编译期间是无法确定的。(动态联编)。对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法。
- 重载要求函数名相同,但是参数列列表必须不不同,返回值可以相同也可以不不同。 覆盖要求函数名、参数列列表、返回值必须相同。
- 在类中重载是同一个类中不同成员函数之间的关系 在类中覆盖则是⼦子类和基类之间不同成员函数之间的关系
- 重载函数的调用是根据参数列表来决定调用哪一个函数 覆盖函数的调用是根据对象类型的不不同决定调用哪一个
- 在类中对成员函数重载是不不能够实现多态 在子类中对基类虚函数的覆盖可以实现多态
- 用于函数参数个数相同的类型不同的情况,如果参数个数不同,则不能那个使用函数模板
- 函数模板自动完成重载函数的过程。只需要使用泛型和具体算法来定义函数,编译器将为程序使用特定的参数类型生成正确的函数定义
- 函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板
template <typename AnyType>
void Swap(AnyType &a,AnyType &a)
{
AnyType temp;
temp=a;
a=b;
b=temp;
}
- 模板不会创建任何函数,而只是告诉编译器如何定义函数
- C++98没有关键字typename,使用的是
template<class AnyType>void Swap(AnyType &a,AnyType &a){...}
- 函数模板不能缩短可执行程序,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,只包含了为程序生成的实际函。使用模板的寒除湿,它使生成多个函数定义更简单,更可靠更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件
对多个不同类型使用同一种算法(和常规重载一样,被重载的模板的函数特征标必须不同)。
template <typename T>
void Swap(T& a,T& b);
template <typename T>
void Swap(T* a,T* b,int n);
- 模板的局限性:编写的模板很可能无法处理某些类型
如1.T为数组时,a=b不成立;T为结构时a>b不成立
- 解决方案:
- C++允许重载运算符,以便能够将其用于特定的结构或类
- 为特定类型提供具体化的模板定义
提供一个具体化函数定义,其中包含所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。
- 该内容在代码重用中有不再重复。
对于函数重载,函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用哪一个函数定义,尤其是有多个参数时
过程:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式的转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
最佳到最差的顺序:
- 完全匹配,但常规函数优先于模板
- 提升转换(例如,char和shorts自动转换为int ,float自动转换为double)。
- 标准转换(例如,int转换为char,long转换为double)。
- 用户定义的转换,如类声明中定义的转换。
完全匹配:完全匹配允许的无关紧要转换
从实参到形参 | 到实参 |
---|---|
Type | Type & |
Type & | Type |
Type[] | * Type |
Type(argument-list) | Type( * )(argument-list) |
Type | const Type |
Type | volatile Type |
Type* | const Type |
Type* | volatile Type |
原来的程序分为三个部分
- 头文件:包含结构声明和使用这些结构的函数的原型//结构声明与函数原型
- 源代码文件:包含与结构有关的函数代码 //函数
- 源代码文件:包含调用与结构相关的函数的代码 //调用函数
这种组织方式也与oop方式一致。
- 一个文件(头文件)包含用户定义类型的定义;
- 另外一个文件包含操纵用户定义类型的函数代码;
这两个文件组成了一个软件包,可用于各种程序中。
- 请不要将函数定义或变量声明放在头文件中,如果其他文件都包含这个头文件,那么同一个函数就会有多次定义,变量也同理,会出错。
- 函数原型。
- 使用#define或const定义的符号常量(头文件中不可以创建变量)
- 结构声明=>因为它们不创建变量
- 模板声明=>模板声明不是将被编译的代码,他们指示编译器如何生成源代码中的函数调用相匹配的函数定义。
- 内联函数-->只有它可以在头文件定义函数。
被声明为const的数据和内联函数有特殊的链接属性
注意:在IDE中
- 不要将头文件加入到项目列表中
- 也不要在源代码文件中使用#include来包含其他源代码文件
在同一文件中只能将同一个头文件包含一次。-->使用预编译指令
#ifndef COORDIN_H_
...
#endif
- 但是这种方法并不能防止编译器将头文件包含两次,而只是让它忽略第一次包含之外的所有内容。大多数标注C和C++头文件都是用各种防护(guarding)方案。否则,可能在一个文件中定义同一个结构两次,这将导致编译错误。
- 编译两个源代码文件的UNIX命令: CC file1.cpp file2.cpp
- 预处理将包含的文件与源代码文件合并:
临时文件: temp1.cpp temp2.cpp
- 编译器创建每个源代码文件的目标代码文件:file1.o file2.o
- 链接程序将目标代码文件(file1.o file2.o)、库代码(Library code)和启动代码(startup code)合并,生成可执行文件:a.out
- 由不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。
- 原因:两个编译器为同一个函数生成不同的名称修饰
- 名称的不同将使链接器无法将一个编译器生成的函数调用与另外一个编译器生成的函数定义匹配。在链接编译模块时,请确保所有对象文件或库都是由同一编译器生成的。
- 链接错误解决的方法:如果有源代码,通常可以用自己的编译器重新编译来消除错误。
C++中的四种存储方案
- 自动存储持续性 :在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。
- 静态存储持续性 :在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。(请注意)它们在整个运行过程中都存在。
- 线程存储持续性(C++11) :当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
- 动态存储持续性 :用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。
作用域和链接性
- 作用域(scope) 描述了名称在文件的多大范围内可见。例如,函数中定义的变量可在该函数中使用,但不能在其他函数中使用;而在文件中的函数定义之前定义的变量则可在所有函数中使用。
- 作用域:局部与全局-->(代码块/文件)
- 作用域为局部的变量只在定义它的代码块中可用。(代码块:由花括号括起的一系列语句,比如:函数体)
- 做英语为全局(也叫文件作用域)的变量在定义位置到文件结尾都可以用。
- 链接性(linkage) 描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享,自动变量的名称没有链接性,因为它们不能共享。
shell在执行程序的时候调用exec函数将命令行参数传递给要执行的程序。
使程序了解进程环境,在执行时分配空间。
2.bss段(Block Start by Symbol)
存放未初始化的全局变量或者静态变量。
3.data段
存放具有明确初始值的全局变量或者静态变量。
存在于程序镜像文件中,由 exec 函数从程序镜像文件中读入内存。
4.text段
CPU执行的机器指令。
堆栈简要概述 栈:系统自动开辟空间,自动分配自动回收,在作用域运行完成后(函数返回时)就会被回收。
堆:由程序员自己申请空间,释放空间,不释放会出现内存泄漏。
栈
1.栈是连续的向下扩展的数据结构,总共只有1M或者2M的空间。空间不足就会异常提示栈溢出。
2.存储自动变量, 函数调用者信息, 包括函数参数(可变参数列表的压栈方向是从右向左), 函数内局部变量, 函数返回值, 函数调用时的返回地址。
堆
1.堆是不连续的向上扩展的数据结构,大小受限于计算机系统虚拟内存的大小。
2.操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
对于大多数系统,会在这块内存空间中的首地址处(一般为一个字节的大小)记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。
由于找到的堆结点的空间大小可能大于申请的大小,系统会自动的将多余的那部分(即内存碎片)重新放入空闲链表中。这就涉及到申请效率的问题。
下面列出5种变量存储方式(引用名称空间之前)
存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
- 自动存储持续性
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性(自动变量不能共享)。
- 自动变量的初始化:可以使用任何声明时其值已知的表达式来初始化自动变量
int x=5;int y=2*x;
- 自动变量和栈:自动变量的数目随函数的开始和结束而增减,因此程序必须在运行是对自动变量进行管理。常用方法是流出一段内存,并将其视为栈。程序使用两个指针来跟踪栈,一个指向栈底,栈的开始位置。另外一个指针指向栈顶,下一个可用内存单元。栈是LIFO(后进先出)的,即最后加入到栈中的变量首先被弹出。
- 寄存器变量-->旨在提高访问变量的速度
关键字register最初由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量
register int count_fast;//request for a register variable
鉴于关键字register只能用于原来就是自动的变量,使用它的唯一原因是,指出程序员想使用一个自动变量,这个变量名可能与外部变量相同
- 静态持续变量
C++也为静态存储持续性提供了三种链接性
- 1.外部链接性(可在其他文件中访问)
- 2.内部链接性(只能在当前文件中访问)
- 3.无链接性(只能在当前函数或代码块中访问)
这三种链接性都在整个程序执行期间一直存在,与自动变量相比,他们的寿命更长。**由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们,编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显示地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。**被称为0初始化
- 例子:
int NUM_ZDS_GLOBAL = 80; //#1
static int NUM_ZDS_ONEFILE = 50; //#2
int main(){
…
}
void fun1(int n){
static int nCount = 0; //#3
int nNum = 0; //#4
}
void fun2(int q){
…
}
#1、#2、#3在整个程序运行期间都存在。在fun1中声明的#3的作用域为局部,没有链接性,这意味着只能在fun1函数中使用它,就像自动变量#4一样。但是,与#4不同的是,即使在fun1没有被执行的时候,#3也保留在内存中。
静态变量初始化
#include<cmath>
int x; //零初始化
int y=5; //常量表达式初始化
long z=13*13; //常量表达式初始化
const double pi=4.0*atan(1.0);//动态初始化,要初始化pi,必须调用函数atan(),这需要等到函数被链接上且程序执行时。(这也是常量表达式初始化)
//C++新增关键字constexpr,这增加了创建常量表达是的方式
链接性为外部的变量通常称为外部变量,它们的存储持续性为静态,作用域为整个文件。
外部变量是函数外部定义的,因此对所有函数而言都是外部的。
例如,可以在main()前面或头文件中定义他们。可以在文件中位于外部定义后面的任何函数中使用它。
因此外部变量也称为全局变量。
全局变量是在所有函数体的外部定义的,程序的所在部分(甚至其它文件中的代码)都可以使用。全局变量不受作用域的影响(也就是说,全局变量的生命期一直到程序的结束)。如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量,那么这个文件可以使用这个数据。
一方面,在每个使用外部变量的文件中,都必须声明它;另外一方面,C++有“单定义规则”,该规则指出,变量只有一次定义。
为满足这种需求,C++提供了两种变量声明。
-
- 一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间。
-
- 一种是引用声明(referencing declaration)或简称为声明(declaration),它不给内存变量分配存储空间。
引用声明使用关键字extern,且不进行初始化;否则,声明未定义,导致分配内存空间:例
double up;//definition,up is 0 定义
extern int bllem;//blem defined elsewhere 声明,blem变量在某处定义了
extern char gr = 'z';//definition because initialized 定义
- 单定义规则并非意味着不能有多个变量名称相同
- 如果函数中声明了一个与外部变量同名的变量,结果将如何呢?
//external1.cpp 文件1
double warning=0.3;//warning defined 定义
//support.cpp 文件2
extern double warning;//use warning from another file 使用外部定义的变量warning
......
void update(double dt)
{
extern double warning;//optional redeclaration
......
}
void local()
{
//定义域全局变量名相同的局部变量都,局部变量将隐藏全局变量
double warning=0.8;//new variable hides external one
......
}
通常情况下,应使用局部变量,然而全局变量也有它们的用处。例如,可以让多个函数可以使用同一个数据块(如月份,名数组或原子量数组)。外部存储尤其适用于表示常量数据,因为这样可以使用关键字const来防止数据修改。
- 全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。
- 这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。
- 由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。 * static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。
这种变量是这样创建的,将static限定符用于代码块中定义的变量。
在两次函数调用之间,静态局部变量的值将保持不变,它同时拥有静态变量和局部变量的特性,即:
- 编译时自动初始化
- 会被放到静态内存的静态区
- 只能在局部被访问
有时候我们需要在两次调用之间对变量进行保存,通常的想法是定义一个全局变量来实现。但这样一来变量就不属于函数本身了,而受全局变量的控制。静态局部变量正好可以解决这个问题,静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下一次赋新值
- auto(在C++11中不再是说明符):在C++11之前,可以在声明中使用关键字auto来指出变量为自动变量;但在C++11中,auto用于自动类型推断。
- register:用于在声明中指示寄存器存储,在C++11中,它只是显式地指出变量是自动的。
- static:关键字static被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。
- thread_local(C++11新增):可以用static或extern结合使用,关键字thread_local指出变量持续性与其所属的持续性相同。thread_local变量之于线程,由于常规静态变量至于整个程序。
- mutable:关键字mutable的含义根据const来解释
mutable:可以用来指出,即使结构(或类)变量为const,其某个成员也可以被修改。
struct data
{
char name[30];
mutable int accesses;
...
}
const data veep={"claybourne clodde",0,...};
strcpy(veep.name,"ytttt"); //not allowed
veep.accessses++; //allowed
- const:它表明,内存被初始化后,程序便不能再对他进行修改。
- volatile: volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
- const限定对默认存储类型稍有影响。在默认情况下,全局变量的链接性为外部的,但const全局变量的链接性为内部的
const int fingers = 10;//same as static const init fingers=10;
int main(){
...
}
- 原因:C++这样子修改了常量类型的规则,让程序员更轻松
- 假如,假设将一组常量放在头文件中,并在同一程序的多个文件中使用该头文件。那么预处理器将头文件中的内容包含到每个源文件后,所有的源文件都将包含类似下面的定义:
const int fingers=10;
const char* warning ="wak!";
- **如果全局const声明的链接性像常规变量那样是外部的,则根据单定义规则,这将出错(二义性)。**也就是说只能有一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外只有未使用extern关键字的生命才能进行初始化。
- 然而,由于外部定义的const数据的链接性为内部的,因此可以在所有文件中使用相同的声明。
- 内部链接性意味着每个文件都有自己一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件所私有的,这就是能够将常量定义放在头文件中的原因。
- 和C语言一样,C++不允许在一个函数中定义另外一个函数=>因此所有函数的存储持续性都自动为静态的,即整个程序执行期间都一直存在。
- 在默认情况喜爱,函数的链接性为外部的,即可以在文件间共享。
- 实际上可以使用extern关键字来指出函数是在另外一个文件中定义的,不过这是可选的。
- 使用关键字static将函数链接性改为内部链接性,使其只能在本文件中使用,必须在原型和函数定义中同时使用该关键字。
-
单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性味外部的函数来说,这意味着在多文件程序中,只能有一个文件包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
-
内联函数不受这种规则的约束,这允许程序员能够将内联函数的定义放在头文件中,这样包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。
假设在程序的某个文件中调用一个函数,C++将到哪里寻找函数定义?
- 如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;
- 否则,编译器(包括链接程序)将在所有文件中查找。
- 如果在程序文件中找不到,编译器将在库中搜索。这意味着,如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数。
链接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名词只对应一个函数,因此这很容易实现。为满足内部需要,C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法称为C语言链接性(C language linkage)。但在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称纠正或名称修饰,为重载函数生成不同的符号名称。例如,spiff(int)转换为—_spiff_i
,而将spiff(double, double)转换为_spiff_d_d。这种方法称为C++语言的链接性(C++ language linkage)。
如果要在C++程序中使用C语言预编译的函数,将出现什么情况呢?例如,假设有如下代码:spiff(22);它在C库文件中的符号名称为_spiff,但对于我们的C++链接程序来说,C++查询约定是查找符号民称_spiff_i。为解决这样的问题,可以用函数原型来指出要使用何种约定:
extern “C” void spiff(int);//使用C语言链接性
extern void spoff(int);//使用C++语言的链接性(通过默认方式指出)
extern “C++” void spaff(int);//使用C++语言的链接性(通过显式指出)
C和C++链接性是C++标准制定的说明符,但实现可以提供其他语言链接性说明符。
使用C++运算符new(或C函数malloc())分配的内存,这种内存被称为动态内存,**动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另外一个函数中将其释放。**其分配方式要取决于new和delete在何时以何种方式被使用。通常编译器使用三块独立的内存:
- 一块用于静态变量(可能再细分)
- 一块用于自动变量
- 另外一块用于动态存储
- 虽然存储方案概念不是用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量(自动指针变量,静态指针变量),指针变量还是有作用域和链接性的
如果要为内置的标量类型(int、double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起。
int *pi = new int(6);
要初始化常规结构或数组,需要使用大括号的列表初始化,这要求编译器支持C++11。
struct where{double x, double y, double z};
where *one = new where{2.5, 5.3, 7.2};
int *ar = new int[4] {2, 4, 7, 6};
在C++11中,还可将初始化列表用于单值变量:
int *pin = new int {6};
- 在最初的10年中,C++让new失败时返回空指针,但现在将引发std::bad_alloc异常。
运算符与函数:
//分配函数(allcation function);
void *operator new(std::size_t);//函数
void *operator new[](std::size_t);//函数
//释放函数(deallocation function);
void *operator delete(void *);//函数
void *operator delete[](void *);//函数
int *pi=new int;//运算符
int *pi=new(sizeof(int));//函数
int *pi=new int[40];//运算符
int *pi=new(40*sizeof(int));//函数
替换函数:
- 有趣的是,C++**将这些函数(分配函数,释放函数)**称为可替换的(replaceable)。这意味着如果您有足够的知识和意愿,可为new和delete提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new运算符,但它将调用您定义的new()函数。
通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。下面的代码段演示了new运算符的4种用法:
#include <new>
char buffer1[50];//静态数组
char buffer2[500];
struct chaff
{
char dross[20];
int slag;
};
chaff *p1, *p2;
int *p3, *p4;
p1=new chaff; //place structure in heap
p3=new int[20]; //place int array in heap
p2=new (buffer1) chaff; //place structure in buffer1
p4=new (buffer2) int[20]; //place int array in buffer2
上述代码从buffer1中分配空间给结构chaff,从buffer2中分配空间给一个包含20个元素的int数组。
- 定位new运算符的其他形式 就像常规new调用一个接受一个参数的new函数一样,标准定位new调用一个接收两个参数的new函数。
int * p1=new int;//调用 new(sizeof(int))
int * p2=new(buffer) int;//调用 new(sizeof(int),buffer)
int * p3=new(buffer) int[40];//调用new(40*sizeof(int),buffer)
- 定位new运算符不可替换,但可重载。至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被定义为new。
在C++中,名称可以是变量,函数,结构,枚举,类以及类的结构成员
声明区域(declaration region)
可以在其中进行声明的区域
- 在函数外面声明全局变量=>对这种变量,其声明区域为其声明所在的文件。对于在函数声明的变量=>其声明区域为其声明所在的代码块。
潜在作用域(potential scope)
变量潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
- 变量并非在其潜在作用域的任何位置都是可见的。
- 例如,它可能被另外一个嵌套声明区域中声明的同名变量隐藏
- 例如,在函数声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一文件中声明的全局变量(对于这种变量,声明区域为整个文件)。
- 变量对程序而言可见的范围被称为作用域(scope)。
即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会和另一个名称空间中的名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。
- 关键字namespace
- 名称空间可以是全局的,也可以位于另一个名称空间中,但是不能位于代码块中。因此在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
- 除用户定义的名称空间,还存在另外一个名称空间全局名称空间(global namespace)。它对应文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中
- using声明使特定的标识符可用:
using std::cout;//将cout添加到它所属的声明区域中,即使得cout能够在main函数中直接使用
- using编译指令使整个名称空间可用:
using namespace std;//使得std空间中所有的名称都可以直接使用
- 使用using声明时,就好像声明了相应的名称一样,如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。
- 然而,使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。如果使用using编译指令倒入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
一般来说,使用using声明要比使用using编译指令更加安全,这是由于它只能导入指定的名称,如果该名称与局部名称发生冲突,编译器将发出指示。
using编译指令导入所有的名称,包括可能并不需要的名称,如果与局部名称发生冲突,则局部名称将覆盖名称空间版本而编译器不发出警告! 另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。所以我们平时自己写程序时先怼一个using namespace std;上去可能并不是一个很好的决定。
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口-->C++程序员将接口(类定义)放在头文件中
- 类方法定义:描述如何实现成员函数-->并将实现(类方法代码)放在源代码文件中
细节:
- 使用#ifndef等来访问多次包含同一个文件
- 将类的首字母大写
- 控制访问关键字:private public protected
- C++对结构进行了扩展,使之具有与类相同的特性。他们之间唯一的区别是:结构的默认访问类型是public,类为private
- 通常,数据成员被放在私有部分中=>数据隐藏;成员函数被放在公有部分中=>公有接口
成员函数两个特殊的特征:
- 定义类成员函数时。使用作用域解析符(::)来标识函数所属的类;
- 类方法可以访问类的private组件。
class Stock
{
private:
char company[30];
int shares;
double share_val;
double total_val;
void set_tot(){total_val = shares * share_val;}
public:
Stock();
Stock(const char * co , int n = 0 , double pr = 0.0);
~Stock(){}
void buy(int num , double price);
void sell(int num , double price);
void update(double price);
void show() const;
const Stock & topval(const Stock & s) const;
};
- set_tot()只是实现代码的一种方式,而不是公有接口的组成部分,因此这个类将其声明为私有成员函数(即编写这个类的人可以使用它,但编写带来来使用这个类的人不能使用)。
- 内联方法,
- 其定义位于类声明中的函数都将自动成为内联函数。因此Stock::set_tot()是一个内联函数。
- 在类声明之外定义内联函数
class Stock
{
private:
...
void set_tot();
public:
...
};
inline void Stock::set_tot(){
total_val = shares * share_val;
}
- 内联函数有特殊规则,要求每个使用它们的文件都对其进行定义。确保内联定义对多个文件程序中的所有文件都可用的最简便方法是:将内联定义放在头文件中
- 如何将类方法应用于对象?(对象,数据和成员函数) 所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员。但同一个类的所有对象共享一组类方法,即每种方法只有一个副本。
- 要使用类,要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。
- 实现了一个使用stock00接口和实现文件的程序后,将其与stock00.cpp一起编译,并确保stock00.h位于当前文件夹中
- 类成员函数(方法)可通过类对象来调用。为此,需要使用成员运算符句点。
原因:数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员(私有部分数据)。程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数才能将对象初始化。---类构造函数
- 声明和定义构造函数
//construtor prototype with some default argument
Stock(const string &co,long n=0,double pr=0.0);//原型
原型声明位于类声明的公有部分。
构造函数可能的一种定义
Stock::Stock(const string &co,long n,double pr)
{
company = co;
if(n>0)
{
std::cerr<<"Number of shares can't be negative;"
<< company <<" shares set to 0.\n";
shares = 0;
}
else
shares=n;
share_val=pr;
set_tot();
}
- 注意“参数名co,n,pr不能与类成员相同.构造函数的参数表示不是类成员,而是赋给类成员的值。
- 区分参数名和类成员:一种常见的做法是在数据成员名中使用m_前缀 string m_company;;另外一种常见的做法是,在成员名中使用后缀_ string company_;
- 显式调用
Stock food = Stock1("World cabbage",250,1.25);
- 隐式调用
Stock garment("Furry Mason",50,2.5);
//等价于
Stock garment= Stock("Furry Mason",50,2.5);
- 每次创建类对象(甚至使用new动态分配内存)时,C++都是用类结构函数。
Stock *pstock= new Stock("Electroshock Games",18,19.0);//对象没有名称,但可以使用指针来管理对象
未提供显示初始值是,用来创建对象的构造函数。例:
Stock fluffy_the_cat;//use the default constructor
- 当且今当没有定义任何构造函数时,编译器才会提供默认构造函数。
- 为类定义了构造函数后,程序员就必须为它提供默认构造函数
- 如果提供了非默认构造函数(如Stock(const string &co,long n,double pr);),但没有提供构造函数,下面声明将出错(禁止创建未初始化对象)
Stock stock1;
方法1:给已有构造函数函数的所有参数提供默认值
Stock(const string &co="Error",int n=0,double pr=0.0);
方法2:通过函数重载来定义另外一个构造函数---一个没有参数的构造函数
Stock();
为Stock类提供一个默认构造函数:
//隐式初始化所有对象,提供初始值
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
- 使用默认构造函数:
Stock first;//隐式地调用默认的构造函数
Stock first = Stock();//显式地
Stock * prelief=new Stock;//隐式地
- 然而不要被废默认构造函数的隐式形式所误导:
Stock first("Concrete Conglomerate");//调用构造函数
Stcok second(); //声明一个函数
Stock third; //调用默认构造函数
- 对象过期是,程序将自动调用该特殊的成员函数。析构函数完成清理工作
- 如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
什么时候调用析构函数?这由编译器决定,不应在代码中显示地调用析构函数
- 如果创建的是静态存储类对象,其析构函数将在程序结束时自动被调用。 2 .如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。
- 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。 程序可以创建临时变量对象来完成特定操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
- 总的来说:**类对象过期时(需要被销毁时),析构函数将自动被调用。**因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认构造函数。
只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起。
Stock hot_tip = {"Derivatives Plus Plus",100 ,45.0};//构造函数
Stock jock {"Sport Age Storage,Inc"};//构造函数
Stock temp{};//默认构造函数
前两个声明中,用大括号括起的列表与下面的构造函数匹配:
Stock(const string &co,long n=0,double pr=0.0);//原型
因此,用该构造函数来创建这两个对象。创建对象jock时,第二和第三个参数将默认值为0和0.0。第三个声明与默认构造函数匹配,因此将使用该构造函数创建对象temp。
void Stock::show() const;//promises not to change invoking object
以这种方式声明和定义的类成员函数被称为const成员函数。就像应景可能将const引用和指针作函数参数一样,只要类方法不修改调用对象,就应该将其声明为const
有的方法可能涉及两个对象,在这种情况下需要使用C++的this指针(比如运算符重载)
提出问题:如何实现:定义一个成员函数,查看两个Stocl对象,并返回股价高的那个对象的引用。
- 最直接的方法是,返回一个引用,该引用指向股价总值较高的对象,因此,用于比较的方法原型如下:
const Stock & topval(const Stock & s) const;//该函数隐式地访问一个对象,并返回其中一个对象
- 第一个const:由于返回函数返回两个const对象之一的引用,因此返回类型也应为const引用
- 第二个const:表明该函数不会修改被显式访问的对象
- 第三个const:表明该函数不会修改被隐式访问的对象
调用:
top = stock1.topval(stock2);//隐式访问stock1,显式访问stock2
- this 指针用来指向调用成员函数的对象(this被作为隐藏参数传递给方法)。
- 每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象
- 如果方法需要引用整个调用对象,可一个使用表达式
*this
。
实现:
const Stock & topval(const Stock & s) const
{
if(s.total_val>total_val)
return s;
else
return *this;
}
Stock stocks[4]={
Stock("NanoSmart",12,20.0),
Stock("Boffo Objects",200,2.0),
Stock("Monolothic Obelisks",130,3.25),
Stock("Fleep Enterprises",60,6.5)
};
回顾:
- 全局(文件)作用域,局部(代码块)作用域
- 可以在全局变量所属的任何地方使用它,而局部变量只能在其所属的代码块中使用。函数名称的作用域也可以是全局的,但不能是局部的。
- 在类中定义的名称(如类数据成员和类成员函数名)的作用域为整个类。
- 类作用域意味着不能从外部直接访问类成员,公有函数也是如此。也就是说,要用调用公有成员函数,必须通过对象。
- 使用类成员名时,必须根据上下文使用,直接成员运算符(.),间接成员运算符(->)或者作用域解析符(::)
下面是错误代码
class Bakery
{
private:
const int Months=12;//错误代码
double cots[Months];
}
- 这是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。因此在创建对象之前,并没有用于存储值的空间。
解决:
- 方法一:使用枚举
class Bakery
{
private:
enum{Months=12};
double costs[Months];
}
- 这种方式声明枚举并不会创建类数据成员。也就是说,所有对象都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到他时,编译器将用12来替换它。
- 方法二:使用关键字static
class Bakery
{
private:
static const int Months=12;
double costs[Months];
}
- 这将创建一个名为Months的常量,**该常量与其他静态变量存储在一起,而不是存储在对象中。**因此,只有一个Months常量,被所有Bakery对象共享。
传统的枚举存在一些问题,其中之一是两个枚举定义的枚举量可能发生冲突。
enum egg{Small,Medium,Large,XLarge};
enum t_shirt{Small,Medium,Large,Xlarge};
- 这将无法通过编译因为egg Small和t_shirt Small位于相同的作用域内,他们将发生冲突。
enum class egg{Small,Medium,Large,XLarge};
enum class t_shirt{Small,Medium,Large,Xlarge};
- 作用域为类后,不同枚举定义的枚举量就不会发生冲突了。
- class也可以用关键字struct来代替
使用:
egg choice = egg::Large;
t_shirt Floyd=t_shirt::Large;
注意:作用域内枚举不能隐式地转换为整型,下面代码错误
int ring = Floyd;//错误
但是必要时可以进行显式转换
int Floyd = int(t_shirt::Small);
- 以抽象的方式描述数据类型,而没有引入语言和细节
- 运算符重载或函数多态---定义多个名称相同但特征标(参数列表)不同的函数
- 运算符重载---允许赋予运算符多种含义
运算符函数:operator op(argument-list)
示例:
//有类方法:
Time Sum(const Time &t)const;
//定义:
Time Time::Sum(const Time & t)const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes/60;
sum.minutes%=60;
retrun sum;
}
返回值是函数创建一个新的Time对象(sum),但由于sum对象是局部变量,在函数结束时将被删除,因此引用指向一个不存在的对象,返回类型Time意味着程序将在删除sum之前构造他的拷贝,调用函数将得到该拷贝
- 运算符重载,只需把上述函数名修改即可
Sum()的名称改为operator+()
//调用:
total=coding.Sum(fixing);
//运算符重载后调用
1. total=coding.operator+(fixing);
2. total=coding+fixing;
//t1,t2,t3,t4都是Time对象
t4=t1+t2+t3;
下面是可重载的运算符列表:
运算符 | 分别 |
---|---|
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
下面是不可重载的运算符列表:
.
:成员访问运算符.*
,->*
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符
- 重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型.这防止用户标准类型重载运算符
- 使用运算符时不能违反运算符原来的语句法则,例如,不恩那个将秋末运算符(%)重载成一个操作数。
- 不能创建新的运算符
- 不能重载下面的运算符
sizeof
:sizeof运算符.
:成员运算符::
:作用域解析运算符?:
:条件运算符typeid
:一个RTTI运算符const_cast
:强制类型转换运算符dynamic_cast
:强制类型转换运算符static_cast
:强制类型转换运算符reinterpret_cast
:强制类型转换运算符
- 下面运算符只能通过成员运算符函数进行重载
=
:赋值运算符()
:函数调用运算符[]
:下标运算符->
:通过指针访问类成员的运算符
C++控制类对象私有部分的访问。通常,公有方法提供唯一的访问途径。
- C++提供了另外一种形式的访问权限:友元
- 友元函数
- 友元类(15章)
- 友元成员函数(15章)
- 友元函数:通过让函数成为类的友元,可以赋予该函数与类成员函数相同的访问权限。
- 问:为何需要友元?为类重载二元运算符是(带两个参数的运算符)常常需要友元函数。将Time对象乘以实数就属于这种情况之前我们有运算符重载:
A = B * 2.75;//Time operator*(double n)const;
如果要实现
A=2.75 * B;//不对应成员函数 cannot correspond to a member function
因为2.75不是TIme类型的对象。左侧炒作书应是调用对象
- 解决:
- 告知每个人(包括程序员自己),只能按 B * 2.75这种格式编写。
- 非成员函数(非成员函数来重载运算符),非成员函数不是由对象调用的,它所使用的所有值(包括对象)都是显式参数。
有函数原型:
Time operator * (double m,const Time &t);
使用:
A=2.75 * B;或 A=operator *(2.75,B);
- 问题:非成员函数不能访问类的私有数据,至少常规非成员函数不能访问
- 解决:友元函数(非成员函数,但其访问权限与成员函数相同。)
将其原型放在类声明中,并在原型声明前加上关键字friend
- 声明:
friend Time operator * (double m,const Time & t);
。该原型声明意味着下面两点:
- 虽然,operator* ()函数是在类中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
- 虽然,operator* ()函数不是成员函数,但它与成员函数的访问权限相同。
- 定义:不要使用Time::限定符,不要再定义中使用关键字friend
Time operator*(double m,const Time & t)
{
Time result;
long totalminutes=t.hours*mult*60+t.minutes*mult;
resut.hours = totalminutes/60;
result.minutes=totalminutes%60;
return result;
}
- 注:不必是友元函数(不访问数据成员也能完成功能)
Time operator * (double m,const Time & t)
{
return t*m;//调用了Time operator*(double n)const
}
常用友元:重载座左移运算符
- 使用一个Time成员函数重载<<
trip<<cout;//(trip是Time对象)这样会让人困惑
- 通过使用友元函数,可以像下面这样重载运算符:
void operator<<(ostream & os,const Time& t)
{
os<<t.hours<<"hours"<<t.minutes<<"minute";
}
- 该函数成为Time类的一个友元函数(operator<<()直接访问Time对象的私有成员),但不是ostream类的友元(从始至终都将ostream对象作为一个整体来使用)
按照上面的定义,下面语句会出错:
cout<<"Trip Time:"<<trip<<"(Tuesday)\n"//不能这么做
应该修改友元函数返回ostream对象的引用即可:
ostream& operator<<(ostream & os,const Time& t)
{
os<<t.hours<<"hours"<<t.minutes<<"minute";
return os;
}
按照上面的定义,下面可以正常运行:
cout<<"Trip Time:"<<trip<<"(Tuesday)\n"//正常运行
#include<fstream>
ofstram fout;
fout.open("Savetime.txt");
Time trip(12,40);
fout<<trip;//等价于operator<<(fout,trip);
有构造函数Stonewt(double lbs);
可以编写下列代码:
stonewt mycat;//创建一个对象
mycat=19.6;//使用了Stonewt(double lbs)构造函数创意了一个临时对象
上面使用了一个Stonewt(double lbs)构造函数创建了一个临时对象,然后将该对象内容复制到了mycat中,这一过程(19.6利用构造函数变成类对象)需要隐式转换,因为是自动进行的,而不需要显式强制转换。
- --->只接受一个参数类型的构造函数定义了从参数类型到类类型的转换
注意:只有接受一个参数的构造函数才能作为转换函数,然而,如果第二个参数提供默认值,它便可用于转换int
Stonewt(int stn,double lbs=0);
这种自动特性并非总是合乎需要的,因为会导致意外的类型转换。
- 新增关键字explicit,用于关闭这种自动特性,也就是说,可以这样声明构造函数:
explicit Stonewt(double lbs);//关闭了上面的隐式转换,但允许显式转换,即显式强制类型转换
Stonewt mycat;
mycat =19.6;//错误代码
mycat = Stonewt(19.6);//这里是调用构造函数
mycat =(Stonewt)19.6;//这里是前置类型转换
- 当构造函数只接受一个参数是,可以使用下面的格式来初始化类对象。
Stonewt incognito=2.75;
这等价于前面介绍过的另外两种格式:(这两种格式可用于接收多个参数的构造函数) Stonewt incognito(2.75); Stonewt incognito = Stonewt(2.75);
- 下面函数中,Stonewt和Stonewt&形参都与Stonewt实参匹配
void display(const Stonewt & st,int n)
{
for(int=0;i<n;i++)
{
cout<<"WOW!";
st>show_stn();
}
}
- 语句display(422,2);中
- 编译器先查找自动类型转换中42转Stone的构造函数
Stonewt(int)
- 不存在
Stonewt(int)
的话,Stonewt(double)构造函数满足这种要求因为,编译器将int转换为double
- 构造函数只用于某种类型到类类型的转换,要进行相反的转换,必须用到特殊的C++运算符---转换函数
- 转换函数必定是类方法
- 用户定义的强制类型转换,可以向使用强制类型转换那样使用它们。
Stonewt wolf(285,7);
double host = double(wolfe);//格式1
double thinker=(double)wolfe;//格式2
- 也可以让编译器来决定如何做:
Stonewt wolf(20,3);
double star =wells;//隐式转换
opeator typeName();
- 转换函数必定是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
例如:转换函数为double类型的原型如下
operator double();//不要返回类型也不要参数
- 头文件中声明:
operator int() const;
operator double() const;
- cpp文件中定义:
Stonewt::opeator int() const
{
return int (pounds+0.5);//四舍五入
}
Stonewt::opeator double() const
{
return pounds;//四舍五入
}
C++中,int和double值都可以被赋值给long变量,下面语句被编译器认为有二义性而拒绝了。
long gone = poppins;//注:poppins是Stonewt对象
//但是还是可以进行强制类型转换
long gone = (double)poppins;
long gone =int (poppins);
- 方法1:C++98中,关键字
explicit
不能用于转换函数,但C++11消除了这种限制。因此,在C++11中,可将转换运算符声明为显示的:
class Stonewr
{
...
//conversion functions
explicit operator int() const;
explicit operator double() const;
};
有了这些声明后,需要前置转换时,将调用这些运算符。
- 方法2:用一个功能相同的非转换函数替换转换函数即可,但仅在被显式调用时,该函数才会执行。也就是说,可以将:
Stonewt::operator int(){return int(pounds+0.5);}
替换为
int Stonewt stone_to_int(){return int(pounds+0.5);}
这样下面语句为非法的:
int plb=popins;
需要转换时只能调用stone_to_int():
int plb =poppins.stone_to_int();
- 动态内存和类
- -->让程序运行时决定内存分配,而不是在编译时决定。
- ---->使用new和delete运算符来动态控制内存
- ------>在类中使用这些运算符将导致许多新的编程问题。这种情况下,析构函数将是必不可少的,而不再是可有可无的。
- -------->有时候还必须重载赋值运算符。
- 1.默认构造函数,如果没有定义构造函数;记得自己定义了构造函数时,编译器不会再提供默认构造函数,记得自己再定义一个默认构造函数。 带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。
- 2.默认析构函数,如果没有定义;用于销毁对象
- 3.复制(拷贝)构造函数,如果没有定义;用于将一个对象赋值到新创建的对象中(将一个对象初始化为另外一个对象)。用于初始化的过程中,而不是常规的赋值过程。
- 每当程序生成对象副本时(函数按值传递对象,函数返回对象时),编译器都将使用复制构造函数
- 编译器生成临时对象是,也将使用复制构造函数
默认的复制构造函数的功能--->逐个复制非静态成员(成员复制也叫浅复制,给两个对象的成员划上等号),复制的是成员的值;如果成员本身就是类对象,则将使用这个类的复制构造函数复制成员对象,静态成员变量不受影响,因为它们属于整个类,而不是各个对象
- 浅复制面对指针时会出现错误,在复制构造函数中浅复制的等价于sailor.len=sport.len;sailor.str=sport.str;前一个语句正确,后一个语句错误,因为成员char* str是指针,得到的是指向同一字符串的指针!!!
- 当出现动态内存分配时,要定义一个现实复制构造函数--->进行深度复制(deep copy)
StringBad::StringBad(const StringBad & st)
{
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
}
- 4.赋值运算符,如果没有定义;赋值运算符只能由类成员函数重载的运算符之一。将已有的对象赋值给另外一个对象时(将一个对象复制给另外一个对象),使用赋值运算符。
原型:class_name & class_name::operator==(const class_name &);
接受并返回一个指向类对象的引用。
- 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制
- 解决:提供赋值运算符(进行深度复制)定义,其实现业余复制构造函数相似,但有一些差别
//代码1:先申请内存,再delete
CMyString& CMyString::operator=(const CMyString& str)
{
if(this==str)
{
char *temp_pData=new char[strlen(str.m_pData)+1)];
delete[]m_pData;
m_pData=temp_pData;
strcpy(m_pData,str.m_pData);
}
return *this;
}
//代码2:调用复制构造函数
CMyString& CMyString::operator=(const CMyString& str)
{
if(this==str)
{
CMyString strTemp(str);//复制构造函数创建临时对象,临时对象失效时会自动调用析构函数
char* pTemp=strTemp.m_pData;//创建一个指针指向临时对象的数据成员m_pData
strTemp.m_pData=m_pData;//交换
m_pData=pTemp;//交换
}
return *this;
}
- 5.地址运算符,如果没有定义;
- 6.移动构造函数(move construtor),如果没有定义;
- 7.移动赋值运算符(move assignment operator)。
- 1.静态函数:静态函数与普通函数不同,它只能在声明它的文件中可以见,不能被其他文件使用。定义静态函数的好处:静态函数不会被其他文件使用。其他文件中可以定义相同名字的函数,不会发生冲突。
- 2.静态类成员函数:与静态成员数据一样,我们可以创建一个静态成员函数,它为类的全部服务,而不是为某一个类的具体对象服务。静态成员函数与静态成员数据一样,都是在类的内部实现,属于类定义的一部分。普通成员函数一般都隐藏了一个this指针,this指针指向类的对象本身。
- 3.静态成员函数由于不是与任何对象相联系,因此不具有this指针,从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数
- 1.出现在类体外的函数不能指定关键字static;
- 2.静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
- 3.非静态成员函数可以任意地访问静态成员函数和静态数据成员;
- 4.静态成员函数不能访问非静态成员函数和非静态数据成员
- 5.由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比,速度上会有少许的增长
- 6.调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。
- 1.如果构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
- 2.new和delete必须相互兼容,new对应于delete,new[]对应于delete[]
- 3.如果有多个构造函数,则必须以相同的方式使用new,要么中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new来初始化指针,而在另外一个构造函数中初始化为空(0或nullptr),这是因为delete(无论是带括号,还是不带括号)可以用于空指针。
- 4.应定义一个复制构造函数,通过深度复制将一个对象初始化为另外一个对象。
- 5.应当定义一个赋值运算符,通过深度复制将一个对象复制给另外一个对象。
- 1.首先,返回对象将调用复制构造函数(给新创建的临时对象复制(初始化)),而返回引用不会
- 2.其次,返回引用指向的对象因该在调用函数执行时存在。
- 3.返回作为参数输入的常量引用,返回类型必须为const,这样才匹配。
Class_name* ptr = new Class_name;
调用默认构造函数
//使用new运算符创建一个512字节的内存缓冲区
char* buffer =new char[512];//地址:(void*)buffer=00320AB0
//创建两个指针;
JustTesting *pc1,*pc2;
//给两个指针赋值
pc1=new (buffer)JustTesting;//使用了new定位符,pc1指向的内存在缓冲区 地址:pc1=00320AB0
pc2=new JustTesting("help",20);//使用了常规new运算符,pc2指向的内存在堆中
//创建两个指针;
JustTesting *pc3,*pc4;
//给两个指针赋值
pc3=new (buffer)JustTesting("Bad Idea",6);//使用了new定位符,pc3指向的内存在缓冲区 地址:pc3=00320AB0
//创建时,定位new运算符使用一个新对象覆盖pc1指向的内存单元。
//问题1:显然,如果类动态地为其成员分配内存,该内存还没有释放,成员就没了,这将引发问题。
pc4=new JustTesting("help",10);//使用了常规new运算符,pc4指向的内存在堆中
//释放内存
delete pc2;//free heap1
delete pc4;//free heap2
delete[] buffer//free buffer
- 解决问题1,代码如下:
pc1=new (buffer)JustTesting;
pc3=new (buffer+sizeof(JustTesting))("Bad Idea",6);
- 问题2:
将delete用于pc2,pc4时,将自动调用为pc2和pc4指向的对象调用析构函数;问题2:然而,将的delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数
- 解决问题2:显式调用析构函数
pc3->~JustTesting;
pc1->~JustTesting;
- 在类声明的结构、类或枚举被认为是被嵌套在类中,其作用域为整个类
- 这种声明不会创建数据对象,而是指定了可以在类中使用的类型。
- 1.如果声明是在类的私有部分进行的,则只能在这个类中使用被声明的类型。
- 2.如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型 例如,如果Node是在Queue类的公有部分声明的,则可以在外部声明Queue::Node类型的变量。
如果定义变量时没有指定初始值,则变量被默认初始化。默认值由变量类型和定义变量的位置决定。
- 如果是内置类型的变量未被显示初始化,它的值由定义位置决定。定义于任何函数体外的变量被初始化为0。
//不在块中
int i;//正确,i会被值初始化为0,也称为零初始化
- 定义于函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或其他形式的访问此类型将引发错误
1 {//在一个块中
2 int i;//默认初始化,不可直接使用
3 int j=0;//值初始化
4 j=1;//赋值
5 }
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员。 由编译器提供的构造函数叫(合成的默认构造函数)。 合成的默认构造函数将按照如下规则初始化类的数据成员。
- 如果存在类内初始值(C++11新特性),用它来初始化成员。
class CC
{
public:
CC() {}
~CC() {}
private:
int a = 7; // 类内初始化,C++11 可用
}
- 否则,没有初始值的成员将被默认初始化。
- 使用成员初始化列表的构造函数将覆盖相应的类内初始化
- 对于简单数据成员,使用成员初始化列表和在函数体中使用复制没什么区别
- 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高
如果Classy是一个类,而mem1,mem2,mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员。
Classy::Classy(int n,intm):mem1(n),mem2(0),men3(n*m+2)
{
//...
}
- 1.这种格式只能用于构造函数
- 2.必须用这种格式来初始化非静态const数据成员(至少在C++之前是这样的);
- 3.必须用这种格式来初始化引用数据成员
- 数据成员被初始化顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关
- 1.派生类对象可以使用非私有的基类方法
- 2.基类指针(引用)可以在不进行显示转换的情况下指向(引用)派生类对象(反过来不行);基类指针或引用只能用来调用基类方法,不能用来调用派生类方法。
- 3.不可以将基类对象和地址赋给派生类对象引用和指针。
1.首先创建基类对象; 2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数。 3.派生类构造函数应初始化新增的数据成员。 注意:可以通过初始化列表语法知名指明要使用的基类构造函数,否则使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类的析构函数,然后在调用基类的析构函数。
RetedPlayer::RetedPlayer(unsigned int r,const string & fn,const string &ln, bool ht)//:TableTennisPlayer()等价于调用默认构造函数
{
rating = r;
}
- 经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后。它在派生类中将自动生成虚方法。然而,在派生类中使用关键字virtual来指出哪些函数是虚函数也不失为一个好方法。
- 如果要在派生类中重新定义基类的方法,通常将基类方法声明为虚。这样,程序根据对象类型而不是引用或指针类型来选择方法版本,为基类声明一个虚的析构函数也是一种惯例,为了确保释放派生类对象时,按正确的顺序调用析构函数。
- 虚函数的作用:基类指针(引用)指向(引用)派生类对象,会发生自动向上类型转换,即派生类升级为父类,虽然子类转换成了它的父类型,但却可正确调用属于子类而不属于父类的成员函数。这是虚函数的功劳。
在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法,如果没有使用作用域解析符,有可能创建一个不会终止的递归函数。如果派生类没有重新定义基类方法,那么代码不必对该方法是用作用域解析符(即该方法只有在基类中有)。
函数名联编(binding):将代码中的函数调用解释为执行特定的代码块。
- 在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。
- 在C++中,由于函数重载的缘故,这个任务更繁杂,编译器必须查看函数参数以及函数名才能确定使用哪个函数。
静态联编(static binding)
- 在编译过程中进行联编,又称为早期联编
动态联编(dynamic binding)
- 编译器在程序运行时确定将要调用的函数,又称为晚期联编
- 编译器对虚方法使用静态联编,因为方法是非虚的,可以根据指针类型关联到方法。
- 编译器对虚方法使用动态联编,因为方法是虚的,程序运行时才知道指针指向的对象类型,才来选择方法。(引用同理)
- 例如,如果类不会用作基类,则不需要动态联编。
- 同样,如果派生类不重新定义基类的任何方法,也不需要动态联编。
- 通常,编译器处理函数的方法是:给每个对象添加一个隐藏成员--指向函数地址数组的指针(vptr)
使用虚函数时,在内存和执行速度上有一定的成本,包括: a.每个对象为存储地址的空间; b.对于每个类,比那一期都将创建一个虚函数地址表(数组)vtbl; c.对于每个函数调用,都需要执行一项额外的操作,到表中查找地址。虽然非虚函数的效率比虚函数稍高,但不具有动态联编的功能
- 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
- 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不是用为引用或者指针类型定义的方法。这个成为动态联编或者晚期联编。这种行为非常重要。因为这样基类指针或引用可以指向派生类对象。
- 如果定义的类将被用作基类,则应该将那些在派生类中重新定义的类方法生命为虚的。
- 1.构造函数不能是虚函数,派生类不能继承基类的构造函数,将类构造函数声明为虚没什么意义。
- 2.析构函数应当是虚函数,除非类不用作基类。
1.当子类指针指向子类是,析构函数会先调用子类析构再调用父类析构,释放所有内存。 2.当父类指针指向子类时,只会调用父类析构函数,子类析构函数不会被调用,会造成内存泄漏。(基类析构函数声明为虚,可以使得父类指针能够调用子类虚的析构函数)所以我们需要虚析构函数,将父类的析构函数定位为虚,那么父类指针会先调用子类的析构函数,再调用父类析构,使得内存得到释放。
- 3.友元不能是虚函数,因为友元不是类成员,只有类成员才是虚函数。
- 4.如果派生类没有重新定义函数。将使用该函数的基类版本。
- 5.重新定义将隐藏方法不会生成函数的两个重载版本,而是隐藏基类版本。如果派生类位于派生链中,则使用最新的虚函数版本,例外的情况是基类版本是隐藏的。总之,重新定义基本的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖其基类声明,无论参数列表是否相同,该操作将隐藏所有的同名方法。
- 1.如果重新定义继承的方法,应确保与原来的原型完全相同,但是如果返回类型是积累的引用或指针,则可以修改为指向派生类的引用或指针(只适用于返回值而不适用于参数),这种例外是新出现的。这种特性被称为返回类型协变(convariance of return type),因此返回类型是随类类型变化的。
//基类
class Dwelling
{
public:
virtual Dwelling & build(int n);
}
//派生类
class Hovel:public Dwelling
{
public:
virtual Hovel & build(int n);
}
- 2.如果基类声明被重载了,则应该在派生类中重新定义所有基类版本。
//基类
class Dwelling
{
public:
//三个重载版本的showperks
virtual void showperks(int a)const;
virtual void showperks(double a)const;
virtual void showperks( )const;
}
//派生类
class Hovel:public Dwelling
{
public:
//三个重新定义的的showperks
virtual void showperks(int a)const;
virtual void showperks(double a)const;
virtual void showperks( )const;
}
如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们, 注意,如果不需要修改,则新定义可知调用基类版本:
void Hovel::showperk()const
{
Dwelling::showperks();
}
- 1.关键字protected与private相似,在类外,只能用公有类成员来访问protected部分中的类成员。
- 2.private和protected之间只有在基类派生的类才会表现出来。派生类的成员可以直接访问基类的保护成员,但是不能直接访问基类的私有成员。
- 1.最好对类的数据成员采用私有访问控制,不要使用保护访问控制。
- 2.对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。
- 在一个虚函数的声明语句的分号前加上 =0 ;就可以将一个虚函数变成纯虚函数,其中,=0只能出现在类内部的虚函数声明语句处。
- 纯虚函数只用声明,而不用定义,其存在就是为了提供接口,含有纯虚函数的类是抽象基类。我们不能直接创建一个抽象基类的对象,但可以创建其指针或者引用。
- 值得注意的是,你也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。但此时哪怕在外部定义了,也是纯虚函数,不能构建对象。
- 派生类构造函数只直接初始化它的直接基类。多继承的虚继承除外。
- 抽象类不能实例化,所以抽象类中不能有构造函数。
- 纯虚函数应该在派生类中重写,否则派生类也是抽象类,不能实例化。
- C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0,
virtual double Area() const=0;//=0指出类是一个抽象基类,在类中可以不定义该函数
- 可以将ABC看作是一种必须的接口。ABC要求具体派生类覆盖其纯虚函数---迫使派生类遵顼ABC设置的接口规则。简单来说是:因为在派生类中必须重写纯虚函数,否则不能实例化该派生类。所以,派生类中必定会有重新定义的该函数的接口。
- 从两个类(具体类concrete)(如:Ellipse和Circle类)中抽象出他们的共性,将这些特性放到一个ABC中。然后从该ABC派生出的Ellipse和Circle类。 这样,便可以使用基类指针数组同时管理Ellipse和Circle对象,即可以使用多态方法
- 就像友元关系不能传递一样,友元关系同样不能继承,基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
- 只有当一个类被用来做基类的时候才会把析构函数写成虚函数。
- 当基类和派生类都采用动态内存分配是,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类
本身是另外一个类的对象。这种方法称为包含(containment),组合(composition),或层次化(laying)
基类的公有成员和保护成员都将成为派生类的私有成员。和公有继承一样,基类的私有成员是会被派生类继承但是不能被派生类访问。基类方法将不会成为派生类对象公有接口的一部分,但可以在派生类中使用它们。
- 1.初始化基类组件
和包含不同,对于继承类的新版本的构造函数将使用成员初始化列表语法,它使用类名(std::string,std::valarry)而不是成员名来表示构造函数
//Student类私有继承两个类派生而来,本来包含的时候两个基类分别是name和score
class Student:private std::string,private std::valarry<double>
{
public:
......
};
//如果是包含的构造函数
Student(const char *str,const double *pd,int n):name(str),score(pd,n)
{
}
//继承类的构造函数
Student(const char *str,const double *pd,int n):std::string(str),std::valarry<double>(pd,n)
{
}
- 2.访问基类的方法
a.包含书用对象(对象名)来调用方法
b.私有继承时,将使用类名和作用域解析运算符来调用方法
double Student::Average() const
{
if(ArrayDb::size()>0)//ArrayDb typedef为std::valarry<double>
return ArrayDb::sum()/ArrayDb::size();
else
return 0;
}
- 3.访问基类对象
使用私有继承时,该string对象没有名称。那么,student类的代码如何访问内部string对象呢? 强制类型转换!
本来子到父自动类型提升,不需要强制类型转换。父到子才需要强制类型转换。但是下面是强制类型转换,原因在第4点那里写着。
由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为S=string对象
//成员方法:打印出学生的名字
//因为不是包含,只能通过强制类型转换
const string & Student::Name()const
{
retrun (const string &) *this;
}
- 4.访问基类友的元函数
用类名显式地限定函数名不适合友元函数,因为友元不属于类。不能通过这种方法访问基类。
解决:通过显示地转换为基类来调用正确的函数
osstream & operator<<(ostream & os,const Student & stu)
{
os << "Score for "<<(const String &) stu <<":\n";//显式地将stu转换为string对象引用,进而调用基类方法
}
引用不会自动转换为string引用 原因:
- a.在私有继承中,未进行显示类型转换的派生类引用或指针,无法赋值给基类的引用或指针。
- b.即使这个例子使用的是公有继承,也必须使用显示类型转换。原因之一是,如果不使用类型转换,下述代码将无法与函数原型匹配从而导致递归调用,os<<stu
- c.由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供函数operator<<()。
- 5.使用包含还是私有继承? 通常,应使用包含来建立has-a关系;如果新需要访问原有的保护成员,或重新定义虚函数,则应使用私有继承。
- 6.保护继承
- 基类的公有成员和保护成员都将成为派生类的保护成员。
- 共同点:和私有继承一样,基类的接口在派生类中也是可用的,但在继承和结构之外是不可用的。
- 区别:使用私有继承时,第三代类将不能使用基类的接口,这是因为公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代将变成保护的,因此第三代派生类可以使用它们。
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
- 7.使用using重新定义访问权限 使用派生或私有派生时,基类的公有成员将成为保护成员或私有成员,假设要让基类方法在派生类外面可用
- 方法1,定义一个使用该基类方法的派生类方法
double Student::sum() const
{
return std::valarray<double>::sum();
}
- 方法2,将函数调用包装在另外一个函数调用中,即使用一个using声明(就像空间名称一样)
class Student::private std::string,private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
}
//using声明只适用于继承,而不适用于包含 //using声明只使用成员名---没有圆括号,函数特征表和返回类型
必须使用关键字public来限定每一个基类,这是因为,除非特别指出,否则编译器将认为是私有派生。(class 默认访问类型是私有,strcut默认访问类型是公有)
- 1.从两个不同的基类继承同名方法。
- 2.从两个或更多相关的基类那里继承同一个类的多个实例。
class Singer:public Worker{...};
class Waiter:public Worker{...};
class SingerWaiter:public Singer,public Waiter{...};
- Singer和Waiter都继承一个Worker组件,因此SingerWaiter将包含两份Worker的拷贝-->通常可以将派生来对象的地址赋给基类指针,但是现在将出现二义性。(基类指针调用基类方法时不知道调用哪个基类方法),第二个问题:比如worker类中有一个对象成员,那么就会出现
- 虚基类使得从多个类(他们的基类相同)派生出的对象只继承一个基类对象。
class Singer:virtual public Worker{...};//virtual可以和public调换位置
class Waiter:public virtual Worker{...;
//然后将SingingWaiter定义为
class SingingWaiter:public Singer,public Waiter{...};
现在,SingingWaiter对象只包含Worker对象的一个副本
- 第一,一些情况下,可能需要基类的多个拷贝;
- 第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;
- 第三,这样做是有缺点的,为了使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码
- 对于非虚基类,唯一可以出现在初始化列表的构造函数是即是基类构造函数。
- 对于虚基类,需要对类构造函数采用一种新的方法。
- 基类是虚的时候,禁止信息通过中间类自动传递给基类,因此向下面构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter。然而,**编译器必须在构造派生对象之前构造基类对象组件;**在下面情况下,编译器将使用Worker的默认构造函数(即类型为Worker的参数没有用!而且调用了Worker的默认构造函数)
SingingWaiter(const Worker &wk,int p=0;int v=Singer:other):Waiter(wk,p),Singer(wk,v){}//flawed
- 如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。
SingingWaiter(const Worker &wk,int p=0;int v=Singer:other):Worker(wk),Waiter(wk,p),Singer(wk,v){}
- 上述代码将显式地调用构造函数worker(const Worker&)。请注意,这种调用是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
- 多重继承可能导致函数调用的二义性。
假如每个祖先(Singer,waiter)都有Show()函数。那么如何调用
- 1.可以使用作用域解析符来澄清编程者的意图:
SingingWaiter newhire("Elise Hawks",2005,6,soprano);
newhire.Singer::Show();//using Singer Version
- 2.然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个show。
P559~P560
- 如果基类是虚基类,派生类将包含基类的一个子对象;
- 如果基类不是虚基类,派生类将包含多个子对象
- 当虚基类和非虚基类混合是,情况将如何呢?
//有下面情况
class C:virtual public B{...};//B为虚基类
class D:virtual public B{...};//B为虚基类
class X: public B{...}; //B为非虚基类
class Y: public B{...}; //B为非虚基类
class M:public C,public D,public X,public Y{...};
- 这种情况下,类M从虚派生祖先C和D那里共继承了一个B类子对象,并从每一个非虚派生祖先X和Y分别继承了一个B类子对象。因此它(M)包含三个B类子对象。
- 当类通过多条虚途径和非虚途径继承了某个特定的基类时,该类包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。(本例子中是1+2=3)
- 使用非虚基类是,规则很简单,如果类从不同的类那里继承了两个或更多的同名函数(数据或方法),则使用该成员名是,如果没有用类名进行限定,将导致二义性。
- 但如果使用的是虚基类,则这样做不一定会导致二义性。这种情况下,如果某个名称**优先于(dominates)**其他所有名称,则使用它时,即使不使用限定符,也不会导致二义性。
class B
{
public:
short q();
...
};
class C:virtual public B
{
public:
long q();
int omg();
...
};
class D:public C
{
...
}
class E:virtual public B
{
private:
int omg();
...
};
class F: public D,public E
{
...
};
- 1.类C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来的。因此F中的方法可以使用q()来表示C::q().(父子类之间有优先级,子类大于父类)
- 2.任何一个omg()定义都不优先于其他omg()定义,因为C和E都不是对方的基类。所以,在F中使用非限定的omg()将导致二义性。
- 3.虚二义性规则与访问规则(pravite,public,protected)无关,也就是说即使E::omg是私有的,不能在F类中直接访问,但使用omg()仍将导致二义性。
- 类模板和模板函数都是以template开头(当然也可以使用class),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。
template <typename 类型参数1,typename 类型参数2,typename 类型参数3>class 类名
{
//TODO
}
- 类模板中定义的类型参数可以用在函数声明和函数定义中,
- 类模板中定义的类型参数可以用在类型类声明和类实现中,
- 类模板的目的同样是将数据的类型参数化。
template <class Type>
class Stack
{
private:
enum {MAX=10};
Type items[MAX];
int top;
public:
Stack();
……
}
template <class Type>
Stack<Type>::Stack()
{
top=0;
}
- Type:泛型标识符,这里的type被称为类型参数。这意味着它们类似于变量,但赋给它们的不是数字,而只能是类型
- 相比于函数模板,类模板必须显式的提供所需的类型。
- 模板不是函数,它们不能单独编译。模板必须与特定的**模板实例化(instantiation)**请求一起使用,为此,最简单的方法是将所有模板信息放在一个文件中,并在要使用这些模板的文件中包含该头文件。
//类声明Stack<int>将使用int替换模板中所有的Type
Stack<int>kernels;
Stack<string>colonels;
模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
- 类模板具体化生成类声明
- 类实例化生成类对象
- 1.隐式实例化(implicit instantiation)
他们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义;
Array<int,100>stuff;//隐式实例化
//在编译器处理对象之间,不会生成隐式实例化,如下
Array<double,30>*pt;//a pointer,no object needed yet
//下面语句导致编译器生成类定义,并根据该定义创建一个对象昂
pt=new Array<double,30>;
- 2.显式实例化(explicit instantiation)
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的实例化
template class ArrayTP<string,100>;
这种情况下,虽然没有指出创建或提及类对象,编译器也将生成类声明(包含方法定义)。和隐式实例化也将根据通用模板来生成具体化。
- 3.显式具体化(explicit specialization)---是特定类型(用于替换模板中的泛型)的定义
格式:template<>class Classname{...}; 有时候,可能需要在特殊类型实例化是,对模板进行修改,使其行为不同。在这种情况下,可以创建显式实例化。
//原来的类模板
template <typename T>class sortedArray
{
...//details omitted
};
当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
//新的表示法提供一个专供const char*类型使用的SortedArray模板
template<>class SortedArray<const char*>
{
...//details omitted
};
- 4.部分具体化(partical specialization)
部分限制模板的通用性
//general template 一般模板
template<class T1,class T2>class Pair{...};
//specialization with T2 set to int部分具体化
template<class T1>class Pair<T1,int>{...};
如果有多个模板可供选择,编译器将使用具体化程度最高的模板
Pair<double,double>p1;//使用了一般的Pair类模板
Pair<double,int>p2;//使用了部分具体化Pair<T1,int>
Pair<int,int>p3//使用了显式实例化Pair<int,int>
也可以通过为指针提供特殊版本来部分具体化现有模板:
template<class T>
class Feen{...};//一般版本的类模板
template<class T*>
class Feen{...};//部分具体化
template<template<typename T>class Thing>class Crab
模板类声明也可以有友元。模板的友元分为3类:
- 非模板友元:
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
- 非约束(unbund)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
- 在模板类中奖一个常规函数声明为友元:
template <class T>
class HasFriend
{
public:
friend void counts();
...
};
上述声明指定counts()函数称为模板所有实例化的友元
- counts()函数不是通过对象调用(它是友元不是成员函数),也没有对象参数,那么如何访问HasFriend对象?
- 1.它可以访问全局对象
- 2.它可以使用全局指针访问非全局对象
- 3.可以创建自己的对象
- 4.可以访问独立于对象的模板类的静态成员函数
- 1.首先,在类定义的前面声明每个模板函数
templatevoid counts(); templatevoid report(T &);
- 2.然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明
template<typename TT>
class HasFriendT
{
...
friend coid counts<TT>();
friend coid report<>(HasFriendT<TT> &);
};
- 3.为友元提供模板定义
- 前一节中的约束模板友元函数在类外面声明的模板的具体化。int类具体化获得int函数具体化,依此类推。通过类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template<typename T>
class ManyFriend
{
...
template<typename C,typename D>friend void show2(C &,D &);
};
- 1.如果能为类型指定别名,浙江爱你个很方便,在模板设计中尤为如此。可使用typedef为模板具体化指定别名
typedef std::array<double,12> arrd;
typedef std::array<int,12> arri;
typedef std::array<std::string,12> arrst;
//使用
arrd gallones;
arri days;
arrst months;
- 2.C++11新增了一项功能---使用模板提供一系列别名
template<typename T>
using arrtype=std::array<T,12>;//template aliases
这将arrtype定义为一个模板别名,可以用它来指定类型
arrtype<double> gallones;
arrtype<int> days;
arrtype<std::string> months;
- C++11允许将语法using=用于非模板。用于非模板是,这种语法与常规typedef等价:
typedef const char *pc1; //typedef syntax/ 常规typedef语法
using pc2=const char*; //using = syntax/ using =语法
例子:模拟电视机和遥控器的简单程序
公有继承is-a关系并不适用。遥控器可以改变电视机的状态,这表明应将Remove类作为TV类的一个友元
- 友元声明 friend class Remote;--->友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。该声明让整个类成为友元并不需要前向(实现)声明,因为友元语句本身已经指出Remote是一个类。
- 友元Remove可以使用TV类的所有成员
- 大多类方法都被定义为内联。代码中,除构造函数外,所有Remove方法都将一个TV对象引用作为参数,这表明遥控器必须针对特定的电视机
- 同一个遥控器可以控制不同的电视机
TV S42;
TV S58(TV::ON);
Remote grey;
grey.set_chan(S42,10);
grey.set_chan(S58,28);
- 某一个类的成员函数作为另外一个类的友元函数
例子:将TV成员中Remote方法Remote::set_chan(),成为另外一个类的成员
class TV
{
friend void Remote::set_chan(TV& t,int c);
...
};
- 问题1:在编译器在TV类声明中看到Remote的一个方法被声明为TV类的友元之前,应先看到Remote类的声明和set_chan()方法的声明。
//排列次序应如下:
class TV;//forward declaration
class Remote{...};
class TV{...};
- 问题2:Remote声明包含内联代码,例如:
void onoff(TV & t){t.onoff();}
由于这将调用TV的一个方法,所以编译器此时必须看到一个TV的类声明,解决:使Remote声明中只包含方法声明,并将实际的定义放在TV类之后
#include<iostream>
class B
{
public :
B()
{
myValue=2;
std::cout<<"B init"<<std::endl;
}
~B()
{
std::cout<<"B end"<<std::endl;
}
//这样做可以
/*B operator+(const B av)
{
B a;
a.myValue=myValue+av.myValue;
return a;
}*/
//也可以这么做
friend B operator+(const B b1,const B b2);
//------------------------------------------------
int GetMyValue()
{
return myValue;
}
//重载<<
friend std::ostream& operator<<(std::ostream& os,B);
private :
int myValue;
};
B operator+(const B b1,const B b2)
{
B a;
a.myValue=b1.myValue+b2.myValue;
return a;
}
std::ostream& operator<<(std::ostream& os,B b)
{
return os<<"重载实现:"<<b.myValue<<std::endl;
}
int main()
{
B b1;
std::cout<<b1;
B b2;
B b3=b1+b2;
std::cout<<b3<<std::endl;
std::cin.get();
return 0;
}
- 内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中,这个例子中内联定义位于头文件中,因此在使用函数的文件中包含头文件可确保将定义放在正确的地方。这可以将定义放在实现文件中,但必须删除关键字inline这样函数的链接性将是外部的
- 在另外一个类中声明的类被称为嵌套类(nested class)
- 包含类的成员函数可以创建和使用被嵌套的对象。而仅当声明位于公有部分,才能在包含类外面使用嵌套类,而且必须使用作用域解析运算符
- 访问权限:嵌套类、结构和美剧的作用域特征(三者相同)
声明位置 | 包含它的类是否可以使用它 | 从包含它的类派生而来的类是否可以使用它 | 在外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,可以通过类限定符来使用 |
- 访问控制
- 1.类声明的位置决定了类的作用域或可见性
- 2.类可见后,访问控制规则(公有,保护,私有,友元)将决定程序对嵌套类成员的访问权限。
//在下面的程序中,我们创建了一个模板类用于实现Queue容器的部分功能,并且在模板类中潜逃使用了一个Node类。
// queuetp.h -- queue template with a nested class
#ifndef QUEUETP_H_
#define QUEUETP_H_
template <class Item>
class QueueTP
{
private:
enum {Q_SIZE = 10};
// Node is a nested class definition
class Node
{
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0) {}
};
Node * front; // pointer to front of Queue
Node * rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int qsize; // maximum number of items in Queue
QueueTP(const QueueTP & q) : qsize(0) {}
QueueTP & operator=(const QueueTP & q) { return *this; }
public:
QueueTP(int qs = Q_SIZE);
~QueueTP();
bool isempty() const
{
return items == 0;
}
bool isfull() const
{
return items == qsize;
}
int queuecount() const
{
return items;
}
bool enqueue(const Item &item); // add item to end
bool dequeue(Item &item); // remove item from front
};
// QueueTP methods
template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
front = rear = 0;
items = 0;
}
template <class Item>
QueueTP<Item>::~QueueTP()
{
Node * temp;
while (front != 0) // while queue is not yet empty
{
temp = front;
front = front->next;
delete temp;
}
}
// Add item to queue
template <class Item>
bool QueueTP<Item>::enqueue(const Item & item)
{
if (isfull())
return false;
Node * add = new Node(item); // create node
// on failure, new throws std::bad_alloc exception
items ++;
if (front == 0) // if queue is empty
front = add; // place item at front
else
rear->next = add; // else place at rear
rear = add;
return true;
}
// Place front item into item variable and remove from queue
template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
if (front == 0)
return false;
item = front->item; // set item to first item in queue
items --;
Node * temp = front; // save location of first item
front = front->next; // reset front to next item
delete temp; // delete former first item
if (items == 0)
rear = 0;
return true;
}
#endif // QUEUETP_H_
- 意外情况
1.程序可能会试图打开一个不可用的文件 2.请求过多内存 3.遭遇不能容忍的值
- 其典型实现是向标准错误流(即cerr使用的错误流)发送信息abnormalprogram termination(程序异常中止),然后终止程序。它返回一个随实现而异的值,告诉操作系统,处理失败。
- 调用abort()将直接终止程序(调用时,不进行任何清理工作)
- 使用方法:1.判断触发异常的条件 2.满足条件时调用abort()
- 1.exit(): 在调用时,会做大部分清理工作,但是决不会销毁局部对象,因为没有stack unwinding。 会进行的清理工作包括:销毁所有static和global对象,清空所有缓冲区,关闭所有I/O通道。终止前会调用经由atexit()登录的函数,atexit如果抛出异常,则调用terminate()。
- 2.abort():调用时,不进行任何清理工作。直接终止程序。
- 3.retrun:调用时,进行stack unwinding,调用局部对象析构函数,清理局部对象。如果在main中,则之后再交由系统调用exit()。
- return返回,可析构main或函数中的局部变量,尤其要注意局部对象,如不析构可能造成内存泄露。exit返回不析构main或函数中的局部变量,但执行收工函数, 故可析构全局变量(对象)。abort不析构main或函数中的局部变量,也不执行收工函数,故全局和局部对象都不析构。 所以,用return更能避免内存泄露,在C++中用abort和exit都不是好习惯。
一种比异常终止更灵活的方法是,使用函数的返回值来指出问题
- C++异常是对程序运行过程中发生的异常情况(例如被0除)的一种响应。异常提供了将控制权从程序的一个部分传递到另外一部分的途径
- 异常机制由三个部分组成
double hmean(double a,double b)
{
if(a==-b)
throw "bad heam() arguments:a=-b not allowed";//throw关键字表示引发异常(实际上是跳转)
return 2.0*a*b/(a+b);
}
- 例子:
#include <iostream>
using std::cout;
using std::cin;
using std::cerr;
int fun(int & a, int & b)
{
if(b == 0)
{
throw "hello there have zero sorry\n"; //引发异常
}
return a / b;
}
int main()
{
int a;
int b;
while(true)
{
cin >> a;
cin >> b;
try //try里面是可能引发异常代码块
{
cout << " a / b = "<< fun(a,b) << "\n";
}
catch(const char *str) 接收异常,处理异常
{
cout << str;
cerr <<"除数为0\n"; //cerr不会到输出缓冲中 这样在紧急情况下也可以使用
}
}
system("pause");
return 1;
}
1.try:try块标识符其中特定的异常可能被激活的代码块,他后面跟一个或者多个catch块.
2.catch:类似于函数定义,但并不是函数定义,关键字catch表明这是给一个处理程序,里面的
const cahr* str
会接受throw传过来错误信息.
3.throw:抛出异常信息,类似于执行返回语句,因为它将终止函数的执行,但是它不是将控制权交给调用程序,而是导致程序沿着函数调用序列后退,知道找到包含try块的函数.
注意:
1.如果程序在try块外面调用fun(),将无法处理异常。
2.throw出的异常类型可以是字符串,或其他C++类型:通常为类类型
3.执行throw语句类似于执行返回语句,因为它也将终止函数的执行。
4.执行完try中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行后面的第一条语句。
5.如果函数引发了异常,而没有try块或没有匹配处理程序时,将会发生什么情况。在默认情况下,程序最终调用abort()函数!
- C++如何处理函数调用和返回的?
1.程序将**调用函数的地址(返回地址)**放入到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始执行。
2.函数调用将函数参数放入到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建的自动变量,则这些自动变量也将被添加到栈中
3.如果被调用的函数调用了另外一个函数,则后者的信息将被添加到栈中,依此类推。
- 假设函数出现异常(而不是返回)而终止,则程序也将释放栈中的内存,但不会释放栈中的第一个地址后停止,而是继续释放,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后的第一条语句,这个过程被称为栈解退。
- 1.stdexcept 异常类(头文件stdexcept定义了其他几个异常类。)
- 该文件定义了
1.logic_error类 2.runtime_error类
他们都是以公有的方式从exception派生而来的。- 1.logic_error类错误类型(domain_error、invalid_argument、length_error、out_of_bounds)。每个类都有一个类似于logic_error的构造函数,让您能够提供一个供方法what()返回的字符串。
- 2.runtime_error类错误类型(range_error、overflow_error、underflow_error)。每个类都有一个类似于runtime_error的构造函数,让您能够提供一个供方法what()返回的字符串。
- 2.bad-alloc异常和new(头文件new)
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的生命,他是从exception类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回一个空指针。
- 3.异常类和继承
1.可以像标准C++库所做的那样,从一个异常类派生出另外一个。
2.可以在类定义中嵌套异常类声明类组合异常。
3.这种嵌套声明本身可被继承,还可用作基类。
- 旨在为程序运行阶段确定对象的类型提供一种标准方式
RTTI的工作原理
C++有三个支持RTTI的元素
1.如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成指向派生类的指针;否则,该运算符返回0---空指针。
2.typeid运算符返回指出对象类型的值
3.type_info结构存储了有关特定类型的信息。
- 1.dynamic_cast运算符是最常用的RTTI组件
他不能回答“指针指向的是哪类对象”这样的问题,但能回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题
用法:Superb* pm = dynamic_cast<Super*>(pg);
其中pg指向一个对象
提出这样的问题:指针pg类型是否可被安全地转换为Super* ?如果可以返回对象地址,否则返回一个空指针。
- 2.typeid运算符和type_info类。
typeid运算符使得能够确定两个对象是否为同类型,使用:如果pg指向的是一个Magnification对象,则下述表达式的结果为bool值true,否则为false;
typeid(Magnification)==typeid(*pg)
type_info类的实现岁厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串;通常(但并非一定)是类的名称。例如下面的语句想爱你是指针pg指向的对象所属的类定义的字符串:
```C++
cout<<"Now Processing type"<<typeid(*pg).name()<<".\n";
4个类型转换运算符:dynamic_cast\const_cast\static_cast\reinterpret_cast
1.dynamic_cast<type_name>(expression)
- 该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),不允许其他转换。
2.const_cast<type_name>(expression)
- 该运算符用于执行只有一种用途的类型转换,即改变之const或volatile其语法与dynamic_cast运算符相同。
3.static_cast<type_name>(expression)
4.reinterpret_cast<type_name>(expression)
- 用于天生危险的类型转换。