注:这一章都是理解记忆性的内容,因此笔者在某些知识点会将自己的理解话语写上,便于可读性和方便理解。
解释:如果头文件包含一个函数定义,然后属于同一程序的其他两个文件中都包含了该头文件,则同一程序中就包含了函数的两个定义,除非函数内联(inline),否则会出错。
< >:如果文件名包含在<>中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;
" “:如果文件名包含在”“中,则编译器将首先查找当前工作目录或源代码目录(取决于编译器),如果在那没找到头文件,则将在标准位置查找。
注:因此在包含自己的头文件时,应使用引号”"而不是尖括号<>。
解释:ifndef - if not defined。这三个如何使用,简要说明如以下三部分。
//#define - 通常使用#define语句来创建符号常量
#define NUMBER 24
//但只要将define作用于名称其实就可以完成该定义了,因此:
#define STUDY_H_
//下面代码片段意味着当且仅当以前没有使用预处理器编译指令
//#define定义名称STUDY_H_时,才处理#ifndef 和#endif之间的语句:
#ifndef STUDY_H_
....
#endif
//综上可以像如下这样使用:
#ifndef STUDY_H_
#define STUDY_H_
(place include file contents here)
#endif
C++使用三种不同的方案(C++11中是四种)来存储数据,这些方案的区别在于数据保留在内存中的时间。
1.1 自动存储持续性
在函数定义中声明的变量(包括函数的参数),其存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完成函数或代码块时,他们的内存就被释放。C++有两种存储持续性为自动的变量。
1.2 静态存储持续性
在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态,他们在整个运行过程中都存在。C++有三种存储持续性为静态的变量。
1.3 线程存储持续性(C++11)
如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
1.4 动态存储持续性
用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时候被称为自由存储或堆。
作用域:名称在文件中的多大范围可见(作用范围-函数/代码块、全局)
链接性:如何在不同的单元中共享(文件间-外链接、文件中-内链接、只能当前函数/代码块使用-无链接性)
2.1 自动变量:作用域为局部,无链接性
int main()
{
int num;
int testNum = 24;
{
cout << "internal: " << endl;
int testNum = 23;
cout << num << " = num" << endl;//输出随机数
cout << testNum << " = testNum" << endl;//输出结果为23 = testNum,此时当前地址的testNum为代码块局部变量,隐藏了之前的定义,新定义可见,就定义不可见。
}
cout << testNum << " = testNum" << endl;//输出结果为24 = testNum,此时当前地址的testNum为全局变量。
}
如上代码所示:①自动变量的初始化是不确定的;②自动变量的数目随着函数的开始和结束而增减,因此程序在运行时对其进行管理,通常是留出一块内存,并将其视栈进行管理变量的增减(这样理解之后,就很好记住自动变量这类数据是存储在栈区当中),栈如下图所示。
2.2 静态持续变量
C++为静态存储持续性变量提供了3种链接性:外部链接性(文件间:可在其他文件中访问)、内部链接性(只能在当前文件中访问)、无链接性(只能在当前函数或代码块种访问)。
2.2.1 与自动变量的区别:区别于自动变量,由于静态变量的数目在程序运行期间时不变的,因此程序不需要使用特殊装置(如栈)来管理它们。编译器通过分配固定的内存块来存储所有的静态变量,这些变量在程序执行期间一直存在(下面例子会说明),如果没有显式地初始化静态变量,其默认值会被设置为0(这也区别于自动变量的“随机数”)。
如下图中展示了静态外部变量、内部变量还有自动变量:a-静态内部变量、b静态外部变量、c自动变量。
2.2.2 静态变量的初始化:分为静态初始化(零初始化、常量表达式初始化)、动态初始化(变量在编译后初始化-个人理解为有如函数调用等类似的情况)。注:常量表达式并非只能使用字面常量的算术表达式,还可以使用sizeof运算符等。
int x;//静态初始化:零初始化
int o = sizeof(int)//静态初始化:零初始化
int y = 5;//静态初始化:常量表达式初始化
long z = 13 * 13//静态初始化:常量表达式初始化
const double pi = 4.0 * atan(1.0);//动态初始化
2.2.3 静态持续性、外部链接性
首先了解C++的单定义规则,即在每个使用外部变量的文件中都必须声明它,但变量只能有一次定义。为了这种需求C++提供了两种变量声明:①定义声明(简称定义),它能给变量分配存储空间;②引用声明(简称声明),它不给变量分配存储空间,因为他引用的是已有的变量,其使用关键字extern。
关键字extern的使用:如果在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他文件中,必须使用extern声明它。
//file1.cpp
int num;
//int num = 16; 如果file1中num初始化了某个值,那么在file2中就不可以再num = 24;
//file2.cpp
#include "file1.cpp"
extern int num = 24;
int main()
{
cout << num << " = num " << endl;//输出24,如果file2中extern int num,此时输出默认值0.
}
外部变量与局部变量:覆盖问题、如何选择
由如下代码块可以看出,外部变量在函数中会由局部变量所覆盖。
//file1.cpp
int num = 24;
//file2.cpp
extern int num;
void globalNum(int it);
void localNum();
using std::cout;
void globalNum(int it)
{
extern int num;
num += it;//此时num为外部(相对于局部称为全局)变量
cout << num << " = num" << endl;
cout << &num << " = address of num" << endl;
}
void localNum()
{
int num = 23;
num += it;//此时num为局部变量
cout << num << " = num" << endl;
cout << &num << " = address of num" << endl;
}
那么既然能使用全局、局部,该怎么选择呢?答:通常情况下,应使用局部变量。这是由于:全局变量是所有的函数都能访问,这样造成了程序的不可靠性。程序越能避免对数据的不必要访问,就越能保持数据的完整性。
2.2.4 静态持续性、内部链接性
将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
如下代码块所示,首先它违反了单定义规则,file2中的定义试图创建一个外部变量,因此程序将包含两个error的定义,错误。
//file1.cpp
int error = 24;
//file2.cpp
#include "file1.cpp"
int error = 23;
//extern int error;
//这样才是正确的,在多文件的程序中,可以且只能在一个文件中定义一个外部变量,其他使用变量的文件只能使用关键字extern声明它。
void errorPrint()
{
cout << error << endl;//failed
}
再如下代码块所示,file2定义了一个静态外部变量,虽然其名称与file1中声明的常规外部变量一样,但是这种情况下静态变量将隐藏常规外部变量,不会出错。(两种链接性)
//file1.cpp
int right = 24;
//file2.cpp
#include "file1.cpp"
static int right = 23;
void rightPrint()
{
cout << right << endl;//uses right defined in file1
}
2.2.5 静态持续性、无链接性
如下代码块所示,在代码块中使用static int total时,将导致局部变量的存储持续性为静态的,这意味着该变量只在当前代码块中可用,并且不处于活动状态时仍然存在,因此其与count的结果不一样。
如果初始化了静态局部变量,则程序只在启动时进行了一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。
void test()
{
using namespace std;
static int total = 0;
int count = 0;
cout << total++ << " = total" << endl;
cout << count++ << " = count" << endl;
}
int main()
{
for(int i = 0; i < 2; ++i)
test();
}
//输出结果如下:
0 = total
0 = count
1 = total
0 = count
2.2.6 说明符与限定符
auto(在C++11中不再是说明符):以前的auto用于显示地指出变量为自动存储,在C++11中使用auto关键字可以要求编译器对变量的类型进行自动推导
register:
static:关键字static被用于整个文件时表示其变量是内部链接性;被用于局部声明中时,表示局部变量的存储持续性为静态的。
extern:关键字extern声明是引用声明,即声明引用在其他地方定义的变量。
thread_local(C++11新增):关键字thread_local指出变量的持续性与其所属线程的持续性相同,其变量之于线程犹如常规静态变量之于整个程序。
mutable:关键字mutable,例如在结构体中可以让const限定符后的结构体的mutable成员完成修改。
在C++中,const限定符对默认存储类型稍有影响,在默认情况下,全局变量的链接性为外部,但const全局变量的链接性为内部。所以如果程序员希望某个常量的链接性为外部,则可以使用extern来覆盖默认的内部链接性:
extern const int state = 50;
2.2.7 函数和链接性
C++不允许在一个函数中定义另一个函数,因此所有函数的持续性都自动为静态的,即在程序执行期间一直存在。**在默认的情况下,函数的链接性为外部,即可以在文件间分享,使用关键字static可以将函数的链接性设置成内部的,即只能在一个文件中使用。**此时,必须同时在原型和函数定义中使用extern。和变量一样,也存在静态函数覆盖外部定义的情况。
static int private(int num);
....
static int private(int num)
{
....
}
实际上,可以在函数原型中使用关键字extern指出函数是另一个文件中定义的,但这是可选的,这需要文件必须作为程序的组成部分被编译,或链接程序搜索的库文件。
单定义规则也适用于非内联函数,每个非内联函数程序只能包含一个定义。
针对内联函数:允许程序员将内联函数的定义放在头文件中,这样包含了头文件的每个函数都有其定义,C++要求同一个函数的所有内敛定义必须相同。
2.2.8 存储方案和动态分配
通常,编译器使用三块独立的内存:用于静态变量(可能再细分)、用于自动变量、用于动态存储。
动态内存是由运算符new、delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数重分配动态内存,而在另一个函数中将其释放。
注:通常程序结束时,由new分配的内存都将被释放,但是情况并不总是这样,在某些情况下,请求大型内存块将导致代码块在程序结束时不会被自动释放。因为最佳做法是使用delete手动释放。
①下面介绍使用new运算符的初始化:
//内置的标量类型(如int/double...)
int *pi = new int(6);// *pi set to 6
double *pd = new double(99.99);//*pd set to 99.99
//常规结构和数组,需要用大括号的列表初始化(C++11)
struct where {
double x; double y; double z};
where *one = new where {
1.1, 1.2, 1.3};
int *ar = new int[4] {
1, 2, 3, 4};
//在C++11中还可以使用列表初始化用于单值常量
int *pi = new int{
6};
double *pd = new double{
99.99};
//其他形式
int *pi = new(sizeof(int));
int *pi = new(40 * sizeof(int));// int *pi = new int[40];
②new失败:new可能找不到请求的内存量,现在这将引发异常std::bad_alloc。
③定位new运算符
new负责在堆(heap)中找到一个能够满足要求的内存块,new运算符还有个变体可以指定要使用的位置,被称为定位(placement)new运算符。
#include
char buffer[50];//静态内存,位于delete管辖范围外
int main()
{
int offset = 1;
int *p = new (buffer) int[20];
//也可以加上偏移量
//int *p = new (buffer + 1*sizeof(int)) int[20];
...
delete[] p//don't work
}
注:delete只能用于常规new运算发分配的堆内存,而对定位new运算符,不属于其管辖内存范围。
//file1.cpp
namespace kobe
{
int number;
std::string last_name;
struct lakers
{
std::string fname;
std::string lname;
};
void restInPeace();
}
namespace pau
{
using namespace kobe;
struct LAKERS
{
lakers fname;
double amount;
}
...
}
//file2.cpp
#include "file1.cpp"
namespace kobe
{
using std::cout;
void restInPeace()
{
cout << "RIP, Kobe Bean Bryant " << endl;
}
}
//using编译指令
using namespce std
//using声明
int x;
std::cin >> x;
std::cout << x << std::endl;
//using声明
using std::cin;
using std::cout;
using std::endl;
cin >> x;
cout << x << endl;
注:
Ⅰ. 不要在头文件中使用using编译指令。首先,这样做掩盖了哪些名称可用;另外包含头文件的顺序可能影响程序的行为。如果非得要使用编译指令using,应该放其在所有预处理器编译指令#include之后。
Ⅱ. 导入名称时,首选使用作用域解析运算符或者using声明的方法。
Ⅲ. 对于using声明,首选将作用域设置为局部而不是全局。
感谢大家的浏览,如有不正确或者需要添加的地方,欢迎评论区或者私信交流。这些都是笔者一点一滴的心血,来个点赞评论收藏吧!哈哈哈如需转载及其他目的,望先告知笔者。
最后,放一张我本科时得到国奖写的个人事迹的其中一块的截图,来标记下我的第一篇博文。也希望你我在学习过程中、成长过程中都永怀一颗曼巴的心♥