作用域
作用域是一个标识符在程序正文中有效的区域。C++中标识符的作用域有函数原型作用域、局部作用域(块作用域)、类作用域和命名空间作用域。
①函数原型作用域
在函数原型声明时形式参数的作用范围就是函数原型作用域。
如:double area(double radius);//函数声明
标识符radius的作用范围在函数area形参列表的左右括号之间,在程序的其他地方不能引用这个标识符。
注意:由于在函数原型的形参列表中起作用的只是形参类型,标识符并不起作用,因此是允许省去的。但是考虑到程序的可读性,通常还是要在函数原声明时给出形参标识符。
②局部作用域(块作用域)
在函数内部一对大括号中声明的变量的作用域为局部作用域。在函数定义时,形参列表中的参数属于该函数的局部变量,作用域为局部作用域。
③类作用域
类的成员函数可以访问类的所有数据成员。在类外,类的对象可以访问类的公有成员(公有成员,在类外也不能直接使用,必须通过类的对象来访问)。
④命名空间作用域
命名空间定义:
namespace 命名空间名
{
命名空间内的各种声明(函数声明、类声明、变量声明……)
}
一个命名空间确定了一个命名空间作用域,凡是在该命名空间之内声明的、不属于前面所述各种作用域的标识符,都属于该命名空间作用域。在该命名空间内部可以直接引用当前命名空间中声明的标识符(前提是标识符的声明在引用之前)。
如果需要引用其他命名空间的标识符(变量、函数、类……),需要使用下面的语法:
①命名空间名::标识符名
如:
std::cout << “hello” <<std::endl;//每次都要在使用的标识符前加上命名空间名,使用不方便。
当两个命名空间中有相同的标示符,并且都使用的using namespace命令,那么在引用标示符时,必须采用“命名空间名::标识符名”引用来加以区分。否则编译时报二义性错误。
如:
namespace A
{
inta = 9;
}
namespace B
{
inta = 6;
}
using namespace A;
using namespace B;
//cout << a << endl; // error C2872: 'a' : ambiguous symbol
cout << A::a << endl; //OK
cout << B::a << endl; //OK
在一个文件中,全局命名空间和匿名命名空间中可以定义同名的标示符。但是如果直接使用同名的标示符,编译时会报二义性错误。
②using 命名空间名::标识符名
如:
using std::cout;//使用std命名空间里的cout标识符
cout << “hello” << std::endl;//要使用的所有标识符都得使用一次using语句(using std::cout;),否则就只能采用第一种方式使用了(如std::endl)。
③using namespace 命名空间名;
如:
using namespace std; //使用std命名空间里的所有标识符
cout << “hello” << endl;//可以直接使用std命名空间里的所有标识符
命名空间可以嵌套。命名空间分为全局命名空间和匿名命名空间。全局命名空间是默认的命名空间,在显示声明的命名空间之外声明的标识符都在一个全局命名空间中(如:在显示声明的命名空间之外声明的全局变量、全局函数、全局类……)。匿名命名空间是一个需要显示声明的没有名字的命名空间。在一个源文件中没有办法访问其他源文件的匿名命名空间,但可以直接引用本源文件中的所有匿名空间中的所有标示符(前提是命名空间的定义在引用之前)。具有命名空间作用域的变量也称为全局变量。
可见性
程序运行到某一点,能够引用到的标识符,就是该处可见的标识符。
作用域之间的大小关系:
命名空间作用域 > 类作用域 > 局部作用域(块作用域)> 函数原型作用域
作用域可见性的一般规则:
①标识符要声明在前,引用在后
②在同一个作用域中,不能声明同名的标识符
③在没有互相包含关系的不同作用域中声明的同名标识符,互不影响
④如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见。
对象的生存期
①静态生存期
如果对象的生存期与程序的运行期相同,则称它具有静态生存期。在命名空间作用域中声明的对象都具有静态生存期。如果要在函数内部的局部作用域中声明具有静态生存期的对象,则要使用static关键字。
细节:定义时未指定初值的基本类型静态生存期变量,会被初始化为0,而对于动态生存期变量,不指定初始值,意味着初值不确定。
②动态生存期
除了上述两种静态生存期情况,其余的对象都具有动态生存期。局部生存期对象诞生于声明点,结束于声明所在的块执行完毕时。
提示:类的成员对象也有各自的生存期。不用static修饰的成员对象,其生存期都与它们所属对象的生存期保持一致。
类的静态成员
类的静态成员可以实现对象与对象之间的数据共享。
属性
类的属性是描述类的所有对象的共同特征的数据项,对于任何对象实例,它的属性值是相同的。静态数据成员具有静态生存期,可以通过“类名::静态数据成员名”来访问。在类的定义中仅仅对静态数据成员进行引用性声明,必须在命名空间作用域的某个地方使用类名限定定义性声明,这时也可以进行初始化。静态函数成员既可以在类定义中实现,也可以先在类中声明,再在类定义外实现,但static关键字只能写在类定义中,否则编译出错。
提示:之所以类的静态数据成员需要在类定义之外再加以定义,是因为需要以这种方式专门为它们分配空间。非静态数据成员无须以此方式定义,因为它们的空间是与它们所属对象的空间同时分配的。
静态成员函数可以通过类名或对象名来调用,而非静态成员函数只能通过对象名来调用。
习惯:虽然静态成员函数可以通过类名和对象名两种方式调用,但一般习惯于通过类名调用。因为即使通过对象名来调用,起作用的也只是对象的类型信息,与所使用的具体对象毫无关系。
在静态成员函数中访问非静态成员需要指明对象,因为对静态成员函数的调用是没有目的对象的,因此不能像非静态成员函数那样,隐含的通过目的对象访问类的非静态成员。
在静态成员函数中只能访问静态成员,但是在非静态成员函数中可以访问静态成员。静态数据成员可以定义为const,但静态成员函数不能定义为const,否则编译出错。在对类的静态数据成员(包括公有数据成员和私有数据成员)初始化的同时,还可以引用类的其他私有成员。
如:
class Point
{
public:
staticint getP1() //通过静态成员函数访问静态数据成员p1,返回其属性x
{
returnp1->x; //p1是指针,通过"->"来访问
}
staticint getP2() //通过静态成员函数访问静态数据成员p2,返回其属性x
{
returnp2.x; //p2是对象,通过"."来访问
}
staticvoid Destroy()
{
if(p1!= NULL)
{
deletep1;
p1= NULL;
}
}
//析构函数(初始化时,除了通过new调用构造函数外,只要通过其他方式调用了构造函数,析构函数就必须是公有的。)
virtual~Point()
{
cout<< "析构函数" << endl;
}
private:
Point(intx = 0):x(x){cout << "构造函数" << endl;} //私有构造函数
intx; //数据成员x
//静态数据成员p1,类的所有对象共享一份(sizeof(Point)值为8,虚函数和x各占4个字节)
staticPoint *p1;
staticPoint p2; //静态数据成员p2,类的所有对象共享一份
};
Point *Point::p1 = new Point(6); //引用私有构造函数,对其进行初始化
Point Point::p2 = Point(8); //引用私有构造函数,对其进行初始化
int main()
{
cout<< Point::getP1() << endl;//6
cout<< Point::getP2() << endl;//8
Point::Destroy();//显示的释放p1的空间
cout<< sizeof(Point) << endl;//8
return0;
}
单例模式
一个类在任何时刻,只能创建一个实例。
class Point
{
public:
staticPoint* Create(int x, int y) //通过静态成员函数给静态数据成员分配空间
{
if(NULL== m_p) //只有当m_p为NULL的时候,才创建对象。保证在任意时刻只能有一个对象
{
m_p= new Point(x, y); //new 调用构造函数,并给对象分配空间,返回对象的指针
}
if(NULL== m_p) //判断空间是否申请成功
{
cout<< "Create() error!"<< endl; //错误输出
}
returnm_p; //返回对象指针,便于在类外使用
}
staticvoid Destroy()//通过静态成员函数释放静态数据成员的空间
{
if(m_p!= NULL) //当m_p不为NULL时,释放空间
{
deletem_p; //delete 调用析构函数,并释放对象指针空间
m_p= NULL; //将数据成员置为NULL,保证下次还能创建对象
}
}
intGetX() //返回x
{
returnx;
}
intGetY() //返回y
{
returny;
}
private:
Point(intx = 4, int y = 5):x(x), y(y) //私有构造函数
{
cout<< "构造函数" << endl;
}
~Point() //私有析构函数
{
cout<< "析构函数" << endl;
}
intx; //非静态数据成员
inty; //非静态数据成员
staticPoint *m_p; //静态数据成员
};
Point *Point::m_p = NULL;//静态数据成员必须在类定义之外再定义(分配空间)。初始化为NULL
int main()
{
Point*p = Point::Create(6, 8);
cout<< "(" << p->GetX() << ", " <<p->GetY() << ")" << endl;
Point::Destroy();
p= NULL; //避免野指针
p= Point::Create(12, 45);
cout<< "(" << p->GetX() << ", " <<p->GetY() << ")" << endl;
Point::Destroy();
p= NULL; //避免野指针
return0;
}
友元
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。如果友元是一般函数或类的成员函数,称为友元函数;如果友元是一个类,则称为友元类,友元类的所有成员函数都自动成为友元函数。
友元函数是在类中用friend关键字修饰的非成员函数。友元函数可以是一个普通函数,也可以是其他类的成员函数。虽然它不是本类的成员函数。但是在它的函数体中可以通过对象名访问类的私有成员和保护成员。
注意:友元函数是不能传递的。友元关系是单向的。友元关系是不被继承的。
共享数据的保护
⑴常对象:它的数据成员值在对象的整个生存期内不能被改变。常对象必须初始化,而且不能被更新。
注意:在定义一个变量或常量时为它指定初值叫做初始化,而在定义一个变量或常量以后使用赋值运算符修改它的值叫做赋值,请勿将初始化与赋值混淆。
⑵用const修饰的类成员
①常成员函数
使用const关键字修饰的函数为常成员函数,常成员函数的声明格式:
类型说明符 函数名(参数列表)const;
注意:
i. const是函数类型的一个组成部分,因此在函数的定义部分也要带const关键字。
ii. 如果将一个对象说明为常量,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数(这就是C++从语法机制上对常对象的保护,也是常对象唯一的对外接口方式)
iii. 无论是通过常对象还是普通对象调用常成员函数,在常成员函数调用期间,目的对象都被视为常对象,因此常成员函数不能更新目的对象的数据成员(只能使用数据成员的值),也不能针对目的对象调用该类中没有用const关键字修饰的成员函数。(这就保证了在常成员函数中不会更改目的对象的数据成员的值)
iv. const关键字可以用于对重载函数的区分。
提示:如果仅以const关键字区分成员函数的重载,那么通过非const的对象调用该函数,调用不带const关键字的函数;通过const的对象调用该函数,调用带const关键字的函数。如没有不带const关键字的函数,那么都将调用带const关键字的函数。如没有带const关键字的函数,则当通过const的对象调用该函数时,编译出错。
②常数据成员
常数据成员只能通过构造函数的初始化列表来获得初值。
细节:类的静态变量和静态常量都因当在类定义之外加以定义(分配空间)和初始化,但C++标准有一个例外:类的静态常量如果具有整数类型或枚举类型,那么可以直接在类定义中初始化。
③常引用
const 类型说明符 &引用名;
如果用常引用作为形参,便不会意外的发生对实参的更改。
习惯:对于在函数中无须改变其值的参数,不宜使用普通引用方式传递,因为那会使得常对象无法被传入,采用传值方式或传递常引用的方式可以避免这一问题。对于大对象来说,传值耗时较多,因此传递常引用为宜。复制构造函数的参数一般也宜采用常引用传递。
多文件结构和编译预处理命令
决定一个声明放在源文件中还是头文件中的一般原则:
将需要分配空间的定义放在源文件中,例如函数的定义(需要为函数代码分配空间)、命名空间作用域中变量的定义(需要为变量分配空间)等;而将不需要分配空间的声明放在头文件中,例如类声明、外部函数的原型声明、外部变量的声明、基本数据类型常量的声明、static修饰的变量(包括匿名union)等。内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于那些需要被多个编译单元调用的内联函数,它们的代码应该被各个编译单元可见,这些内联函数的定义应该出现在头文件中。
习惯:如果误将分配了空间的定义写入头文件中,在多个源文件包含该头文件时,会导致空间在不同的编译单元中被分配多次,从而在链接时引发错误。
外部变量
如果一个变量除了在定义它的源文件中可以使用外,还能被其他源文件使用,那么就称这个变量时外部变量。在命名空间作用域中,不用extern关键字声明的变量,都是定义性声明;用extern关键字声明的变量,如果同时指定了初始值,则是定义性声明,否则是引用性声明。在不同源文件中,有相同命名空间名的同名外部变量可以有多处声明,但是对变量的定义性声明只能是唯一的。全局命名空间和匿名命名空间属于不同的命名空间,在一个文件中,全局命名空间和匿名命名空间中可以定义相同的标示符。但是不能直接访问同名标识符,否则编译时会报二义性错误。只能通过作用域操作符“::”访问全局命名空间中的标识符,而匿名命名空间中的标识符无法访问。
如:
int a = 9;
namespace
{
inta = 8;
}
int main()
{
//cout<< a << endl;//error 编译出现二义性错误
cout<< ::a << endl;//OK 输出全局命名空间中a——9
return0;
}
在同一源文件中,可以定义多个同名的命名空间,编译时会将它们整合在一起,但是在它们中不能出现对相同标示符的重复定义性声明。
在不同源文件中,有相同命名空间名的命名空间中不能定义性声明同名标识符,否则编译时报二义性错误。
情况1:在不同源文件中,有相同命名空间名的外部变量
//源文件1
namespace D
{
int jj = 8;//定义性声明
}
//源文件2
namespace D
{
extern int jj;//引用性声明
}
//main.cpp
using namespace D;
cout << jj << endl;//成功运行
情况2:在不同源文件中,有相同命名空间名的外部变量
//源文件1
namespace D
{
int jj = 8;//定义性声明
}
//源文件2
namespace D
{
extern int jj = 5;//定义性声明
}
//main.cpp
using namespace D;
cout << jj << endl;//jj重复定义,编译出错
情况3:在不同源文件中,有不同命名空间名的同名外部变量
//源文件1
namespace F
{
int jj = 8;//定义性声明
}
//源文件2
namespace D
{
extern int jj = 6;//定义性声明
}
//main.cpp
using namespace D;
cout << jj << endl;//成功运行
外部函数
在所有类之外声明的函数(也就是非成员函数),都具有命名空间作用域,如果没有特殊说明,这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。当然,也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的。
习惯:通常情况下,变量和函数的定义都放在源文件中,而对外部变量和外部函数的引用性声明则放在头文件中。
将变量和函数限定在编译单元内
ISO C++2.0标准中,用匿名命名空间取代static来限定变量和函数只能在当前文件中使用,不能被外部文件使用。
习惯:应当将不希望被其他编译单元引用的函数和变量放在匿名命名空间中。
标准C++库
标准C++库与组件在逻辑上分为六种类型:
输入输出类
容器类与ADT
存储管理类
算法
错误处理
运行环境支持
提示:包含这些头文件的目的是在当前编译单元中引入所需的引用性声明,而它们的定义则以目标代码的形式存在于系统的运行库中。
习惯:通常情况下,using namespace语句不宜放在头文件中,因为这会使一个命名空间不被察觉的对一个源文件开放。
编译预处理
在编译器对源程序进行编译之前,首先要有预处理器对程序文件进行预处理。预处理器提供了一组编译预处理命令和预处理操作。预处理指令实际上不是C++语言的一部分,它只是用来扩充C++程序设计的环境。所有的预处理指令在程序中都是以“#”来引导,每一条预处理指令单独占用一行,不要用分号结束。预处理指令就可以根据需要出现在程序中的任何位置。
⑴#include指令
①#include <文件名>
按标准方式搜索,文件位于系统目录的include子目录下。
②#include “文件名”
首先在当前目录中搜索,若没有,再按标准方式搜索。
Include指令可以嵌套使用。
⑵#define和#undef指令
⑶条件编译指令
条件编译指令有5种形式
①#if 常量表达式
程序段
#endif
②#if 常量表达式
程序段1
#else
程序段2
#endif
③#if 常量表达式1
程序段1
#elif 常量表达式2
程序段2
……
#elif 常量表达式n
程序段n
#else
程序段n+1
#endif
④#ifdef 标识符
程序段1
#else
程序段2
#endif
⑤#ifndef 标识符
程序段1
#else
程序段2
#endif
⑷defined操作符
defined是一个预处理操作,而不是指令,因此不要以“#”开头。
defined操作符的使用形式:defined(标识符)
①#ifndef HEAD_H
#define HEAD_H
……
#endif
②#if !defined(HEAD_H)
#define HEAD_H
……
#endif
①和②的作用等效,都是防止同一头文件被包含多次。
mutable
mutable修饰的数据成员
①允许在常成员函数中修改被它修饰的数据成员
②“常对象的成员对象被视为常对象”这一预言原则,对mutable修饰的成员对象不适用,被mutable修饰的成员对象在任何时候都不会被视为常对象,这是mutable更一般的含义。
使用mutable关键字一定要有的放矢,一定确定存在需要改变一个成员对象的常成员函数,而且对该成员函数的调用确实不会改变对象状态(下次对象调用该函数,结果不会改变),只有保证了这些,mutable才能够不被滥用。
代码的编译连接和执行过程
①编译
编译生成目标文件
目标文件(.o)
②连接
连接的对象除了用户源程序生成的目标文件外,还有系统的运行库。例如,执行输入输出功能,调用sin和fabs这类标准函数,都需要通过系统运行库。此外,系统运行库中还包括程序的引导代码。在执行main函数之前,程序需要执行一些初始化工作;在main函数返回后,需要通知操作系统程序执行完毕,这些都要由运行库中的代码来完成。
链接后生成的可执行文件的主体,和目标文件一样,也是各个段的信息,只是可执行文件的代码段中所有指令的地址,都是有效地址。符号表可以出现在可执行文件中,也可以不出现,这不会影响到程序的执行,如果可执行文件中出现了符号表,也只是对调试工具有用。
③执行
程序的执行是以进程作为单位的。
第一步:操作系统做一些进程的初始化工作(包括从磁盘加载代码和数据到内存)
第二步:跳转到程序的引导代码(对全局变量进行初始化)
局部变量的初始化内嵌在函数体中。
第三步:开始执行程序(执行main函数)
第四步:程序执行完毕,引导程序通知操作系统完成一些善后工作