5.7 内联函数
有些函数使用频率较高,但代码短小。
将函数声明为inline。
例如:inline int func();
编译器看到inline后,为该函数创建一段代码,以便在后面每次碰到该函数的调用都用相应的一段代码来替换。内联函数可以在一开始仅声明一次。
内联函数中,不能含有复杂的结构控制语句,如switch,while。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码。
另外,递归函数是不能被用来做内敛函数的。
内联函数只适合于只有1--5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数。
5.8 重载函数
C++函数如果在返回类型、参数类型、参数个数、参数顺序上有不同,则认为是不同的。但重载函数如果仅仅是返回类型不同是远远不够的。
重载函数至少在参数个数、参数类型或参数顺序上有所不同。
5.9 默认参数的函数
1. void delay(int loop = 1000);
delay(2500); //loop = 2500;
delay(); //loop= 1000;
2. 默认参数在函数声明中提供,当又有声明又有定义时,定义中不允许默认参数。,如果函数只有定义,则默认参数才可出现在函数定义中。
例如:void point(int = 3 , int = 4);
void point(int x,int y)
{
cout<<x<<endl;
cout<<y<<endl;
}
3.默认参数的顺序规定
如果一个函数中有多个默认参数,则形参分布中,默认参数应从右至左逐渐定义。当调用函数时,只能向左匹配参数。
例如:void func(int a=1,int b,int c = 12,int d =34); //错
void func(int a ,int b=2,intc – 12,int d = 45); //对
func(10,20,30,40);
func(); //错,a没有默认值
func(10);
func(10,20);c,d采用默认值
5. 默认值的规定
int g(int x = fun(a));//ok.允许默认值为函数
7.4 C++中有个memset()函数,她可以一个一个字节的把整个数组设置为一个指定的值。 memset()在mem.h中声明。他把数组的起始地址作为其第一个参数,第二个参数为每个字节的值,第三个参数是数组的长度(字节数,而不是元素个数)。其函数原型:void* memset(void *,int ,unsigned);
8.1 指针的概念
定义指针:int* ptr; int *ptr;int * ptr;他们表示同一个意思。在指针定义中,一个*只能表示一个指针。
用&操作符可以获取变量的地址,指针变量用于存放地址。
*放在可执行语句中的指针之前,称为间接引用操作符,*放在指针定义中时,称指针定义符。
/
经常在CSDN以及其他之类的技术论坛上问关于C++ 头文件的问题。提出这些问题的往往就是那些刚学C++的新手。当初我是菜鸟的时候也问过类似的问题。
现在来看看下面两个include:
#include<iostream> // 这个就是1998年标准化以后的标准头文件
#include<iostream.h> // 这个就是标准化以前的头文件
更本质上的区别就是iostream把标准C++库的组件放在一个名位std的namespace里面。而相对的iostream.h则将这些标准组件放在全局空间里,同时在标准化以后旧有的C标准库也已经经过改造了。
看看下面这两个头文件
// 标准化后经过改造的C的标准库,所有的组件都放在了std中
#include<cstdio>
// 标准化以前C++中的C标准库
#include<stdio.h>
// 在看看这个头文件C标准库下 基于char* 的字符处理函数库
#include<string.h>
// 在标准化以后他变成了这样
#include<cstring>
// 但是很多朋友还看见过这个字符串处理函数库,他包含了新的string class
#include<string>
经过了标准委员会如此大规模手术后,在98年以前出品的C++编译器(BC3.0,BC5.0)上能顺利通过编译的源文件,在支持新标准的编译器上可能无法顺利通过编译也就是很正常的事了。
[起因]
在回过头来看看标准程序库,这个程序库涵盖范围相当广大,提过了许许多多好用的功能。正是因为这样标准程序库中class的名称和函数名与第三方提供的程序库中的class名或是函数名发生名字冲突的可能性大大增大。为了避免这个问题的发生,标准委员会决定将标准程序库中每一样东西都放在namespace std中。但是这么做同时有引来了一个新的问题。很多C++程序代码依赖那些已经存在很多年的C++ “准”标准程序库(C++迟迟未标准化才导致这些情况的发生),例如iosteam.h,complex.h等等。
为了解决这个新出现的问题,标准化委员会决定设计一些新的头文件名,给那些穿上std外衣的组件所使用。把C++头文件的.h去掉,于是就有前面出现的iostream,同样C的头文件也做了相同的处理,同时在前面加上了一个字母c,以表示是C的头文件(感觉上有中种族歧视的感觉)。同时标准化委员会声明就有的C++头文件将不再列于被支持的名单之中了,而旧有的C头文件为了满足“对C的兼容性”这个古老契约,仍然将继续存活下去。
但是,那些编译器厂商不可能去推翻他们客户的旧有编译器(也跟本不会去这么做),所以那些旧有的C++头文件仍然苟延残喘的活了下来,并不断的扰乱那些C++新兵的心智。
下面就是现在大多数C++开发工具表示头文件的组织状态:
1. 旧的C++头文件 比如iostream.h,他们虽然被标准化委员会所抛弃,但由于各大厂商为了各自的商业利益仍然将继续存活下去,这些头文件的内容将不处于namespace std中。
2. 新的C++头文件如iostream虽然提供了和旧有头文件相同的功能,但他的内容都并入了namespace std中,从而有效避免了名字污染的问题。
3. 标准C的头文件如stdio.h继续获得支持,这类文件的内容并未放在std中。
4. C函数库的技能也有对应的新式C++版本,起名称类似cstdio,这类头文件的内容也有幸穿上了std的外衣。
其实标准化以后的标准程序库的改动并不只有这些而已,很多的标准化组件都被“tamplate化”。其中就有元老级人物iostream。标准程序库的问题并不是用一篇,两篇文章就可以说清楚的。如果你像进一步的了解C++的标准程序库的话,你可以看看侯先生的《C++标准程序库》。
数组名本身,没有方括号和下标,她实际上就是一个地址,可以把数组起始地址付给一个指针,通过移动指针来对数组元素进行操作。
int a[100];int* ptr =a;
a[i]=*(a+i) = ptr[i] = *(ptr+i) ;
&a[i] = a+i =ptr+i = &ptr[i] ;
sizeof(*iArray) 表示数组元素类型所占的字节数,该语句可以适应int,char,double 等类型的数组,任凭数组的类型如何变化,这里都不用修改。*iArray表示数组 的第一个元素值。
例如:
int main()
{
int ar[] = {10,20,30,40,50,60};
int* ptr = ar;
int size;
size = sizeof(ar)/sizeof(*ar);
cout<<size <<endl;
cout<<*ar<<endl; // 输出10
return 0 ;
}
数组名是指针常量,区别于指针变量。
8.4 堆内存分配
堆是内存空间。堆是区别于栈区、全局数据区和代码区的一个内存区域。堆允许程序在运行时(而不是在编译时),申请某个大小的内存空间。
程序在编译和链接时不予确定这种在运行时获取的内存空间,这种内存环境随着程序运行时的进展而时大时小,这种内存就是堆内存,所以堆内存是动态的。堆内存也称为动态内存。
void * malloc(size_t size) ;
void free(void *) ;
new 与delete :是c++专有的操作符,他们不用头文件声明。new类似于函数malloc(),分配堆内存,但比malloc更简练。new的操作数为数据类型,她可以带初始化值表或单元个数。new返回一个具有操作数之数据类型的指针。
delete类似于free(),释放堆内存。delete的操作数是new返回的指针,当返回的是new分配的数组时,应该带[]。
例如:int * array;
array = new int[arraysize];
delete []array;
8.5 const 指针
1.指向常量的指针
定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性,而且常量指针的值也是可以修改的。
在指针定义语句的类型前加const,表示指向的对象是常量。
例如:const int a = 78;
const int b = 90;
int c = 12;
const int * pi = &a;
*pi = 25; //error
pi=&b;
*pi = 20; //error
pi=&c ;
*pi = 45; //error
c = 90 ; //ok
指针常量定义“const int * pi = &a;”告诉编译器,*pi是常量,不能将*pi作为左值进行操作。(即可以改变指针地址,但不可以改变该地址所指向的值*pi)
2.指针常量
在指针定义语句的指针名前加const,表示指针本身是常量。
定义“int * const pc = &b;”告诉编译器,pc是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pc可以修改。(即指向的地址不可以变,但指向的地址所包含的值*pc可以变)
3.指向常量的指针常量
const int * const cpc = &ci;
定义“const int * const cpc = &b ;”告诉编译器,cpc和*cpc都是常量,他们都不能作为左值进行操作。
8.6 指针函数:返回指针的函数称为指针函数。指针函
数不能把在她内部说明的具有局部作用域的数据地址
作为返回值。可以返回堆地址,可以返回全局或静态
变量的地址。
8.7 字符指针。由于字符串常量的地址属性,所以两个由相同字符组成的字符串常量的不相等的。
8.8 指针数组:一个数组中每个元素都是一个指针,则为指针数组。
char * pro[];//一班指向二维数组;指针数组名是指向指针的指针。
在程序中,如果一个数组大小不定,则处理指针数组时可以利用在数组末尾设置NULL来解决。
char* pn[]={“frend”,”hleleo ”,”leoajifanf”,NULL};
while(*pn!=NULL){}
NULL 与void*是不同的概念。NULL是一个值,一个指针值,任何类型的指针都可赋予该值。而void*是一种类型,是一种无任何类型的指针。
8.9 命令行参数;
void main(int argc,char* argv[]) //第二个参数也可以写成char **argv
argc表示参数个数,argv表示参数数组。
8.10 函数指针
定义函数指针:int (*func)(char a,char b);
用typedef来简化指针
typedef int(*FUN)(int a , int b);//声明FUN是一个函数指针类型
FUN funp; //funp为一个返回整形和两个整形参数的函数指针。
函数指针可以定义成指针数组。
typedef void (*MenuFun)();
void f1(){}
void f2(){}
void f3(){}
MenuFun fun[] = {f1,f2,f3};
第九章 引用
9.1 引用的概念
引用是一个别名,当建立引用时,程序用另一个变量或对象的名字来初始化她。从那时起,引用作为目标的别名而使用,对引用的改变实际就是堆目标的改动。
int someint;
int& rint = someint ;
引用在声明时必须被初始化,否则报错。
引用一旦初始化,他就维系在一定的目标上,再也不分开。任何对该引用的赋值,都是对引用所维系的目标赋值,而不是将引用维系到另一个目标上。
引用和指针有很大的差别,指针三个变量,可以把她再赋值成指向别处的地址,然而,建立引用时必须进行初始化并且绝不会再关联其他不同的变量。
不能建立引用的数组。
第十章 结构
声明一个结构并不分配内存,内存分配发生在定义这个新数据类型的变量中。
可以用一个结构体给另一个结构体赋值。struct st1,st2;st1 = st2 ;
两个不同结构名的变量是不允许相互赋值的,即使两者包含有相同的成员。
可以将结构变量的地址赋给结构指针,结构指针通过箭头操作符-->来访问结构成员。
当用点操作符时,它的左边应是一个结构变量,当用箭头操作符时,它的左边应是一个结构指针。
箭头操作符合点操作符是可以互换的:(*结构体指针).成员 ;
第二部分 面向对象程序设计
第十一章 类
11.1 c的结构(struct)不含成员函数。C++的类既能包含数据成员,也可以包含函数成员。
C++中,结构是用关键字struct声明的类,默认情况下其成员是公共的(public)。
C++中类与结构的唯一区别是:类(class)定义中默认情况下的成员是private,而结构(struct )定义中默认情况下的成员是public的。
::叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。::可以不跟类名,表示全局数据或全局函数(即非成员函数)。
例如:
int month;
int day;
int year;
void set(int m,int d,int y)
{
::month = m;
::day = d;
::year = y;
}
class Tdate
{
public :
void set(int m,int d,int y)
{
::set(m,d,y);//调用非成员函数
}
private :
int month;
int day;
int year;
};
在类中定义的成员函数一般规模都比较小,语句只有1-5句,而且特别的switch语句不允许使用。他们一般为内联函数,即使没有明确用inline表示。
在C++中,类定义通常在头文件中,因此这些成员函数定义也伴随着进入头文件。我们知道函数声明一般在头文件,而函数定义不能在头文件,因为他们将被编译多次。如果是内联函数,包含在头文件中的允许的,因为内联函数在源程序中原地扩展。由于在类中定义的成员函数默认为内联函数,所以及避免了不能被包含在头文件中的问题。
类的成员函数定义时,类名加在成员函数名之前而不是加在函数的返回类型前。
成员函数必须用对象来调用。另一方面,在成员函数内部,访问数据成员或成员函数无须如此。
一个类中所有对象调用的成员函数都是同一段代码段。涉及到一个问
题,成员函数是如何识别month,day,year的属于哪个对象的
呢??
原来,在对象调用s.Set(2,15,1988)时,成员函数除了接受3个实参
外,还接受到了一个对象s的地址。这个地址被一个隐含是形参this指
针所获取,她等同于执行this= &s。所有对数据成员的访问都隐含的
被加上前缀this->。
因为成员函数是所有对象共享的代码,不是为某一个对象所独占的。所以不能在成员函数内使用某个特定的对象。一个类对象所占据的内存空间由他的数据成员所占据的空间总和所决定。累的成员函数不占据对象的内存空间。
12.3 C++规定与类同名的成员函数是构造函数, 该类的对象创建时,自动被调用。
放在外部定义的构造函数,其函数名前要加上“类名::”,这和别的成员函数定义方法一样。
构造函数不能有返回类型,函数体中叶不允许返回值,但可以有无值返回语句“return ; ”。
析构函数也是特殊的类成员函数,她没有返回类型,没有参数,不能被随意调用,也没有重载。只是在类对象生命周期结束的时候,由系统自动调用。
例如:class XYZ{};
~XYZ(){} //析构函数
析构函数以调用构造函数相反的顺序被调用。
12.5 带参数的构造函数
构造函数可以被重载,C++根据声明中的参数选择合适的构造函数。
12.7 默认构造函数
1)C++规定,每个类必须有一个构造函数,没有构造函数,就不能创建任何对象。
2)若未提供一个类的构造函数,则C++提供一个默认的构造函数,该默认构造函数是个无参构造函数,她仅负责创建对象,而不做任何初始化工作。
3)只要一个类定义 了一个构造函数(不一定是无参构造函数),C++就不再提供默认的构造函数。也就是说,如果为类定义了一个带参数的构造函数,还想要无参构造函数,则必须自己定义。
4)与变量定义类似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为0,否则,对象值是随机的。
因为类是一个抽象的概念,并不是一个实体,并不含有属性值,而只有对象才占有一定的空间,含有明确的属性值。我们只能按照格式“类型 标识符 ; ”去声明类的数据成员。
12.9 我们需要一个机制来表示“构造已分配了空间的对象成员,而不是创建一个新对象成员”。
例如:Student(char *Name = “No Name”,int ssID = 0):id(ssID) //g构造函数
private:
char name[20];
StudentID id;
在Student构造函数头的后面,冒号表示后面要对类的数据成员的构造函数进行调用。id(ssID)表示调用以ssID为实参的StudentID构造函数。如果Student构造函数头冒号后面是id()形式,表示调用StudentID的默认构造函数,并可以省略。
Student(char *Name = “No Name”):id() //g构造函数
或者
Student(char *Name = “No Name”) //g构造函数
冒号语法使得常量数据成员和引用数据成员的初始化称为可能。
class SillyClass
{
public:
SillyClass(int& i):ten(10),refI(i){}
protected:
const int ten; / / 常量数据成员
int& refI; //引用数据成员
}
但不允许这样赋值:
SillyClass() //构造函数
{
ten = 10;
refI = i;
}
下面两种初始化一个整数变量数据成员的方法都可以用:
class SillyClass1
{
public:
SillyClass1()
{
d = 10;
}
protected:
int d;
};
///
class SillyClass2
{
public:
SillyClass2():d(10){} //但不可以这样SillyClass2()":d = 10 {}
protected:
int d;
};
12.10 构造对象的顺序:
1)局部和静态对象,以声明的顺序构造。
程序中,并不是根据运行顺序来决定变量定义的顺序,而是所有的变量和对象都在函数开始执行时,统一定义。统一定义的顺序正是这些变量和对象在函数中出现的顺序。
2)静态对象只被构造一次(不管 其被调用多少次)
3)所有全局对象都在主函数main()之前被构造
4)全局变量构造时无特殊顺序
5)成员以其在类中声明的顺序构造
6)析构函数以与构造函数相反的顺序被调用
第十四章 堆与拷贝构造函数
c++程序的内存格局通常分为四个区:
全局数据区,代码区,栈区,堆区(即自由存储区)。
全局变量、静态数据、常量存放在全局数据区,所有类成员函数和非成员函数代码存放在代码区,为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区,余下的空间都被作为堆区。
14.2 需要new和delete的原因。从C++立场上看,不能用malloc函数的一个原因是,他在分配空间的时候不能调用构造函数。类对象的建立是分配空间、构造结构以及初始化的三位一体,他们统一由构造函数来完成。
14.3 分配堆对象:例如:Tdate *ps; ps = new Tdate; //分配空间并构造她
delete ps;//先析构,然后将空间返还给堆
不必显式的指出new返回的指针类型,因为new知道要分配对象的类型是Tdate。
如果是分配局部对象,则在该局部对象退出作用域时自动调用析构函数。但是堆对象的作用域是整个程序生命期,所以除非程序运行完毕,否则堆对象作用域不会到期。堆对象是在释放堆对象语句delete执行时。
构造函数可以有参数,所以跟在new后面的类型必须跟参数。
从对上分配对象数组,只能调用默认的构造函数,不能调用其他任何构造函数。如果类没有默认构造函数,则不能分配对象数组。
例如:Student * ps = new Student[count] ;
14.4 拷贝构造函数
可用一个对象去构造另一个对象,或者说,用另一个对象初始化一个新构造的对象,例如:
Student s1(“Jenny”);
Student s2 = s1;
这时候,调用构造函数Student(char*)就不合适,新的构造函数的参数应是Student& , 也就是Student(Student & s);
下面的例子介绍了拷贝构造函数的用法:
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(char* pName = "no name" , int ssid = 0)
{
id =ssid;
strcpy(name,pName);
cout<<"Constructing new Student "<<pName<<endl;
}
Student(Student&s)
{
cout<<"Constructing copy of "<<s.name<<endl;
strcpy(name,"copy of "); //自定义的拷贝构造函数里改变了那么的值
strcat(name,s.name);
id = s.id;
}
~Student()
{
cout<<"Distructing "<<name<<endl;
}
protected:
char name[40];
int id ;
};
void fn(Student s)
{
cout<<"in function fn()"<<endl;
}
void main()
{
Student randy("Randy", 100);
cout<<"Calling fn() /n";
fn(randy);
cout<<"Return from fn() /n";
}
14.5 默认拷贝构造函数
在类定义中,如果未提供自己的拷贝构造函数,则c++提供一个默认拷贝构造函数,就像没有提供构造函数时,C++提供默认构造函数一样。
C++提供的默认拷贝构造函数的工作的方法是,完成一个成员一个成员的拷贝。如果成员是类对象,则调用其拷贝构造函数或默认拷贝构造函数。
例如:下面的程序中Tutor类使用了默认的拷贝构造函数:
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(char* pName = "no name")
{
strcpy(name,pName);
cout<<"Constructing new Student "<<pName<<endl;
}
Student(Student&s)
{
cout<<"Constructing copy of "<<s.name<<endl;
strcpy(name,"copy of ");
strcat(name,s.name);
}
~Student()
{
cout<<"Distructing "<<name<<endl;
}
protected:
char name[40];
};
class Tutor
{
public:
Tutor(Student& s):student(s)
{
cout<<"Constructing tutor /n" ;
}
protected:
Student student;
};
void fn(Tutor tutor)
{
cout<<"in function fn() /n" ;
}
void main()
{
Student randy("Randy");
Tutor tutor(randy);
cout<<"Calling fn() /n";
fn(tutor);
cout<<"Return from fn() /n";
}
14.6 浅拷贝与深拷贝
在默认拷贝构造函数中,拷贝的策略是逐个成员一次拷贝。但是,一个类可能会拥有资源,当其构造函数分配了一个资源(例如堆内存)的时候,会发生什么呢?如果拷贝构造函数简单地制作了一个该资源的拷贝,而不对他本身分配,就得面临一个麻烦的局面:两个对象都拥有同一个资源。当对象析构时,该资源将经历两次资源返还。
堆内存并不是唯一需要拷贝构造函数的资源,但她是最常用的一个。打开文件,占有硬件设备(如打印机)服务等要深拷贝。他们也是析构函数必须返还的资源类型。如果你的类需要析构函数来析构资源,则她也需要一个拷贝构造函数。
14.8 无名对象 : 可以直接调用构造函数产生无名对象。
无名对象的三个典型用法:
Student& refs = Student(“Randy”); // 初始化引用
Student s = Student(“Randy”); // 初始化对象定义
fn(Student(“Randy”)); //函数参数
14.9 构造函数用于类型转换
class Student
{
public:
Student(char*);
。。。。。
};
void fn(Student& s);
void main()
{
fn(“Jenny”);
}
这里Student(char*);构造函数同时也在告诉如何将char* 转换成一个Student对象。如果有重载函数fn(char*),则调用fn(“Jenny”)马上匹配了事。但就是因为没有这样的重载函数,于是就被认为是fn(Student(“Jenny”)),最终予以匹配。
但要注意两点:
1)只会尝试含有一个参数的构造函数
2)如果有二义性,则放弃尝试。