参考资料:《C++ Primer Plus 第六版中文版》
笔记作者:Mr.Crocodile
欢迎转载
文章目录
开始学习C++
头文件命名规定
头文件类型 | 约定 | 示例 | 说明 |
---|---|---|---|
C旧风格 | 以.h 结尾 | math.h | C 、C++ |
C++旧风格 | 以.h 结尾 | iostream.h | C++ |
C新风格 | 加上前缀c ,没有扩展名 | cmath | C++ |
C++新风格 | 没有扩展名 | iostream | C++ |
名称空间
//type1 using编译指令
using namespace std;
cout <<"hello"<<endl;
//type2 using声明
using std::cout;
cout <<"hello"<<std::endl;
//type3
std::cout <<"hello"<<std::endl;
- 将
using namespace std;
放在函数定义之前,让文件中所有的函数都能够使用名称空间std
中所有的元素。 - 将
using namespace std;
放在特定的函数定义中,让该函数能够使用名称空间std
中的所有元素。 - 在特定的函数中使用类似
using std::cout;
这样的编译指令,让该函数能够使用指定的元素,如cout
。 - 完全不使用编译指令
using
,而在需要使用名称空间std
中的元素时使用前缀std::
,如std::cout
。
cout
、cin
#include<iostream>
int main(void)
{
std::cout << "Hi\n"<<"ni "<<"hao"<<std::endl; //endl确保程序继续运行前刷新输出,而\n则不能提供这样的保证。
int a,b;
std::cin >> a >> b; //先为a赋值,再为b赋值。
std::cout.put('c');
return 0;
}
- C++提供了两个用于处理输入和输出的预定义对象:
cout
、cin
,它们是istream
和ostream
类的实例,这两个类是在iostream
文件中定义的。为ostream
类定义的插入运算符<<
使得将数据插入输出流成为可能;为istream
类定义的抽取运算符>>
能够从输入流中抽取信息。cout
和cin
都是智能对象,能够根据程序上下文自动将信息从一种形式转换成另一种形式。 - C++提供两种发送消息的方式:一种方式是使用类方法,另一种方式是重新定义运算符(运算符重载),
cout
、cin
使用的就是这种方式。
函数
- 和C一样,C++不允许函数定义嵌套,每个函数定义都是独立的,所有函数的创建都是平等的。
- C++可以使用大量库函数,库函数的编译代码包含在库文件中,编译器编译程序时,它必须在库文件搜索您使用的函数,至于自动搜索哪些库文件,将因编译器而异。
- 使用库函数必须提供函数原型,有两种方式提供:一是在源代码文件中输入函数原型;二是包含相关头文件,其中定义了该库函数的函数原型。(用方式二)
处理数据
简单变量
变量命名规则
- 在名称中只能使用字母、数字和下划线。
- 名称的第一个字符不能是数字。
- 字母区分大小写。
- 不能将C++关键字用作名称。
- 以两个下划线打头,或以一个下划线和一个大写字母打头的标识符是被保留给实现(编译器、库等)使用的;以一个下划线打头的标识符作全局标识符时是被保留给实现(编译器、库等)使用的。
- C++对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。
最后一点使得C++与ANSI C(C99标准)有所区别,后者只保证名称中的前63个字符有意义(在ANSI C中,前63个字符相同的名称被认为是相同的,即使第64个字符不同)。
如果想用两个或更多的单词组成一个名称,通常的做法是用下划线将单词分开(C语言方式),或者从第二个单词开始将每个单词的第一个字母大写(Pascal语言方式)。
整型
C++的基本整型(按宽度递增的顺序排列)分别是char
、short
、int
、long
和C++11新增的long long
,其中每种类型都有符号版本和无符号版本,因此总共有10种类型可供选择。由于char类型有一些特殊属性(它最常用来表示字符,而不是数字),因此本章将首先介绍其他类型。
C++的short
、int
、long
和long long
类型通过使用不同数目的位来存储值,最多能够表示4种不同的整数宽度。如果在所有的系统中,每种类型的宽度都相同,则使用起来将非常方便。如short
总是16位,int
总是32位。不过生活并非那么简单,没有一种选择能够满足所有的计算机设计要求。C++提供了一种灵活的标准,它确保了类型的最小宽度(从C语言借鉴而来):
short
至少16位;int
至少与short
一样长;long
至少32位,且至少与int
一样长;long long
至少64位,且至少与long
一样长。
字节(byte)通常指的是8位的内存单元,从这个意义上说,字节指的就是描述计算机内存量的度量单位,有些人 用术语八位组(octet)表示8位字节。然而,C++对字节的定义与之不同,C++字节由至少能够容纳实现的基本字符集的相邻位组成,也就是说,可能取值的数目必须等于或超过字符数目。
运算符sizeof
和头文件climits
#include<iostream>
#include<climits>
void main(void)
{
std::cout << CHAR_BIT<<std::endl; //8
std::cout << sizeof(int) << std::endl; //4
}
要知道系统中整数的最大长度,可以在程序中使用C++工具来检查类型的长度。sizeof
运算符返回类型或变量的长度,单位为字节。前面说过,“字节”的含义依赖于实现,因此在一个系统中,两字节的int
可能是16位,而在另一个系 统中可能是32位。头文件climits(在老式实现中为limits.h)中包含了关于整型限制的信息,具体地说,它定义了表示各种限制的符号名称。例如,INT_MAX
为int
的最大取值,CHAR_BIT
为字节的位数。
- 可对类型名或变量名使用
sizeof
运算符。对类型名使用sizeof
运算符时,应将名称放在括号中;但对变量名使用该运算符,括号是可选的。 - 头文件climits定义了符号常量来表示类型的限制。编译器厂商提供了climits文件,该文件指出了其编译器中的值。
climits中的符号常量
符号常量 | 表示 |
---|---|
CHAR_BIT | char的位数 |
CHAR_MAX | char的最大值 |
CHAR_MIN | char的最小值 |
SCHAR_MAX | signed char的最大值 |
SCHAR_MIN | signed char的最小值 |
UCHAR_MAX | unsigned char的最大值 |
SHRT_MAX | short的最大值 |
SHRT_MIN | short的最小值 |
USHRT_MAX | unsigned short的最大值 |
INT_MAX | int的最大值 |
INT_MIN | int的最小值 |
UNIT_MAX | unsigned int的最大值 |
LONG_MAX | long的最大值 |
LONG_MIN | long的最小值 |
ULONG_MAX | unsigned long的最大值 |
LLONG_MAX | long long的最大值 |
LLONG_MIN | long long的最小值 |
ULLONG_MAX | unsigned long long的最大值 |
climits文件中包含与下面类似的语句行:
#define INT_MAX 32767
在C++编译过程中,首先将源代码传递给预处理器。在这里,#define
和#include
一样,也是一个预处理器编译指令。该编译指令告诉预处理器:在程序中查找INT_MAX
,并将所有的INT_MAX
都替换为32767。因此#define
编译指令的工作方式与文本编辑器或字处理器中的全局搜索并替换命令相似。修改后的程序将在完成这些替换后被编译。预处理器查找独立的标记(单独的单词),跳过嵌入的单词。也就是说,预处理器不会将PINT_MAXTM
替换为P32767IM
。您可以使用#define
来定义自己的符号常量。然而,#define
编译指令是C语言遗留下来的。C++有一种更好的创建符号常量的方法(使用关键字const
),所以不会经常使用#define
。然而,有些头文件,尤其是那些被设计成可用于C和C++中的头文件,必须使用#define
。
变量初始化
void main(void)
{
//传统C
int a=6;
//C++特性,可用于单值变量、数组、结构、类的初始化
int b{6};
int c={6};
}
大括号内为空值时,变量将被初始化为零。
整型字面量
#include<iostream>
void main(void)
{
int a = 42; //十进制
int b = 042; //八进制
int c = 0x42; //十六进制 等效 0X42
std::cout << a<<" " << b << " " << c<<std::endl; //42 34 66
int d = 42;
//以十进制显示(默认)
std::cout << std::dec;
std::cout << d << std::endl; //42
//以八进制显示
std::cout << std::oct;
std::cout << d << std::endl; //52
//以十六进制显示
std::cout << std::hex;
std::cout << d << std::endl; //2a
}
整型字面量后缀
std::cout << sizeof(55)<<" "<< sizeof(int); //4 4
std::cout << sizeof(55LL) << " " << sizeof(long long); //8 8
std::cout << sizeof(2555555555) << " " << sizeof(2555555555Ul); //8 4
后缀 | 含义 |
---|---|
默认int 类型 | |
U | unsigned int |
L | long |
UL/LU | unsigned long |
LL | long long |
ULL/LLU | unsigned long long |
- 不区分大小写。
- 若类型不能容纳该值,则:
int
->long
->long long
;unsigned int
->unsigned long
->unsigned long long
.
char类型
#include<iostream>
#include<stdio.h>
void main(void)
{
char c = 65;
std::cout << c; //A
//传统C
printf("%c", c); //A
printf("%d", c); //65
}
c明明为数字65,输出时却是字符A,这并非
char
类型的特性,而是因为std::cout
将char
类型智能处理为字符。
char字面量
char a = 'A';
该表示法优于数值编码,不需知道编码方式。
转义序列的编码
字符名称 | ASCII符号 | C++代码 | 十进制ASCII码 | 八进制ASCII码 | 十六进制ASCII码 |
---|---|---|---|---|---|
换行符 | NL (LF) | \n | 10 | 012 | 0xA |
水平制表符 | HT | \t | 9 | 011 | 0x9 |
垂直制表符 | VT | \v | 11 | 013 | 0xB |
退格 | BS | \b | 8 | 010 | 0x8 |
回车 | CR | \r | 13 | 015 | 0xD |
振铃 | BEL | \a | 7 | 07 | 0x7 |
反斜杠 | \ | \\ | 92 | 0134 | 0x5C |
问号 | ? | ? | 63 | 077 | 0x3F |
单引号 | ’ | \’ | 39 | 047 | 0x27 |
双引号 | ” | \" | 34 | 042 | 0x22 |
std::cout << '\''; //'
std::cout << '\047'; //'
std::cout << '\x27'; //'
在可以使用数字转义序列或符号转义序列(如\x8和\b)时,应使用符号序列。数字表示与特定的编码方式(如ASCII码)相关,而符号表示适用于任何编码方式,其可读性也更强。
通用字符名
C++实现支持一个基本的源字符集,即可用来编写源代码的字符集。它由标准美国键盘上的字符(大写和小写)和数字、C语言中使用的符号(如{和=}以及其他一些字符(如换行符和空格)组成。还有一个基本的执行字符集,它包括在程序执行期间可处理的字符(如可从文件中读取或显示到屏幕上的字符)。它增加了一些字符,如退格和振铃。C++标准还允许实现提供扩展源字符集和扩展执行字符集。另外, 那些被作为字母的额外字符也可用于标识符名称中。也就是说,德国实 现可能允许使用日耳曼语的元音变音,而法国实现则允许使用重元音。 C++有一种表示这种特殊字符的机制,它独立于任何特定的键盘,使用的是通用字符名(universal character name)。
通用字符名的用法类似于转义序列。通用字符名可以以\u
或\U
打头。\u
后面是8个十六进制位,\U
后面则是16个十六进制位。这些位表示的是字符的ISO 10646码点(编码)。
如果所用的实现支持扩展字符,则可以在标识符(如字符常量)和字符串中使用通用字符名:
int k\u00F6rper;
cout<<"Let them eat g\u00E2teau.\n";
ö的ISO 10646码点为00F6,而â的码点为00E2。因此,上述C++代码将变量名设置为körper,并显示下面的输出:
Let them eat gâteau.
如果系统不支持ISO 10646,它将显示其他字符或gu00E2teau
,而不是â。 实际上,从易读性的角度看,在变量名中使用\u00F6
没有多大意 义,但如果实现的扩展源字符集包含ö,它可能允许您从键盘输入该字符。 请注意,C++使用术语“通用编码名”,而不是“通用编码”,这是因 为应将\u00F6
解释为“Unicode码点为U-00F6的字符”。支持Unicode的编译器知道,这表示字符ö,但无需使用内部编码00F6。无论计算机使用是ASCII还是其他编码系统,都可在内部表示字符T;在不同的系统中,将使用不同的编码来表示字符ö。在源代码中,可使用适用于所有系统的通用编码名,而编译器将根据当前系统使用合适的内部编码来表示它。
signed char
和unsigned char
与int
不同的是,char
在默认情况下既不是无符号,也不是有符号,是否有符号由C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。如果char有某种特定的行为对您来说非常重要,则可以显式地将类型设置为signed char
或unsigned char
。
如果将char
用作数值类型,则unsigned char
和signed char
之间的差异将非常重要。unsigned char
类型的表示范围通常为0~255,而signed char
的表示范围为−128到127。例如,假设要使用一个char
变量来存储像200 这样大的值,则在某些系统上可以,而在另一些系统上可能不可以。但使用nsigned char
可以在任何系统上达到这种目的。另一方面,如果使用char
变量来存储标准ASCII字符,则char有没有符号都没关系,在这种情况下,可以使用char
。
wchar_t
程序需要处理的字符集可能无法用一个8位的字节表示,如日文汉字系统。对于这种情况,C++的处理方式有两种。首先,如果大型字符集是实现的基本字符集,则编译器厂商可以将char定义为一个16位的字节或更长的字节。其次,一种实现可以同时支持一个小型基本字符集和 一个较大的扩展字符集。8位char可以表示基本字符集,另一种类型 wchar_t
(宽字符类型)可以表示扩展字符集。wchar_t
类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。这种类型与另一种整型(底层(underlying)类型)的长度和符号属性相同。,对底层类型的选择取决于实现,因此在一个系统中,它可能是unsigned short
,而在另一个系统中,则可能是int
。
cin
和cout
将输入和输出看作是char
流,因此不适于用来处理wchar_t
类型。iostream头文件的最新版本提供了作用相似的工具—wcin
和 wcout
,可用于处理wchar_t
流。另外,可以通过加上前缀L来指示宽字 符常量和宽字符串。下面的代码将字母P的wchar_t
版本存储到变量bob 中,并显示单词tall的wchar_t
版本:
wchar_t bob=L'P';
std::wcout<<L"tall"<<std::endl;
在支持两字节wchar_t
的系统中,上述代码将把每个字符存储在一个两个字节的内存单元中。本书不使用宽字符类型,但读者应知道有这种类型,尤其是在进行国际编程或使用Unicode或ISO 10646时。
char16_t
和char32_t
随着编程人员日益熟悉Unicode,类型wchar_t
显然不再能够满足需求。事实上,在计算机系统上进行字符和字符串编码时,仅使用 Unicode码点并不够。具体地说,进行字符串编码时,如果有特定长度和符号特征的类型,将很有帮助,而类型wchar_t
的长度和符号特征随实现而已。因此,C++11新增了类型char16_t
和char32_t
,前者是无符号16位,后者无符号32位。C++11使用前缀u 表示char16_t
字符常量和字符串常量,使用前缀 U表示char32_t
常量。类型char16_t
与/u00F6
形式的通用字符名匹配,而类型char32_t
与/U0000222B
形式的通用字符名匹配。
char16_t ch1=u'q';
char32_t ch2=U'\U0000222B';
与
wchar_t
一样,char16_t
和char32_t
也都有底层类型:一种内置的整型,底层类型的选择取决于实现。
wcin
、wcout
与char16_t
、char32_t
并不适配。
bool
类型
bool a=true;
bool b=false;
字面量true和false都可以通过提升转换为
int
类型,true被转换为1, false被转换为0.
任何数字值或指针值都可以被隐式转换为bool
值:任何非零非空值都被转换为true,为零为空值都被转换为false。
const
限定符
const int LEN=5;
const
要比#define
好:
const
可明确指定类型。const
可以使用C++作用域规则将定义限制在特定的函数或文件中。const
可用于更复杂的类型中(数组、结构).
ANSI C也使用const限定符,这是从C++借鉴来的,但两者有区别:1. 作用域规则(类中、名称空间中)。 2. 在C++(而不是C)中可以用 const值来声明数组长度。
浮点数
浮点类型
//setformat 定点输出 浮点类型
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
std::cout << 0.0001/ 3; //0.000033
类型 | |
---|---|
float | 位宽至少32位,有效数字至少6位 |
double | 位宽至少48位,有效数字至少10位,精度不小于float |
long double | 精度不小于double |
通常位宽,float 32位,double 64位, long double 80、96或128位。
有效数字涉及位分配,如float:1符号位、8指数位、23尾数位。 l o g 10 2 23 ≈ 7 log_{10}2^{23} \approx 7 log10223≈7,有效数字为6确保四舍五入后也数值精准。
这3种类型的指数范围至少是−37到37( 2 127 ≈ 1 0 38 2^{127} \approx 10^{38} 2127≈1038 ),可以从头文件cfloat或float.h中找到系统的限制。
浮点字面量后缀
后缀 | |
---|---|
默认double | |
f/F | float |
l/L | long double |
算术运算符
运算符 | |
---|---|
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
% | 求模 |
类型转换
以{ }方式初始化时进行的转换
int a=66;
char b=a; //隐式类型转换
char c{a}; //报错
int d=66666666666666666; //-385439062
int e = {66666666666666666}; //报错
以{ }方式初始化时,其对类型转换的要求更严格,列表初始化不允许缩窄,即不允许将变量无法表示的值赋值给该变量。
C++11版本校验表
- 如果有一个操作数的类型是long double,则将另一个操作数转换为long double。
- 否则,如果有一个操作数的类型是double,则将另一个操作数转换为double。
- 否则,如果有一个操作数的类型是float,则将另一个操作数转换为float。
- 否则,说明操作数都是整型,因此执行整型提升。
- 在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
- 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型。
- 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
- 否则,将两个操作数都转换为有符号类型的无符号版本。
整型提升
在计算表达式时,C++将bool、char、unsigned char、signed char和short值转换为int。这些转换被称为整型提升(integral promotion)。
short a=1; short b=2; short c=a+b;
C++程序取得a和b的值,并将它们转换为int。然后程序将结果转换为short类型赋给c。通常将计算机最自然的类型选择为int类型,这意味着计算机使用这种类型时,运算速度可能最快。
还有其他一些整型提升:如果short比int短,则unsigned short类型将被转换为int;如果两种类型的长度相同,则unsigned short类型将被转换为unsigned int。这种规则确保了在对unsigned short进行提升时不会损失数据。 同样,wchar_t被提升成为下列类型中第一个宽度足够存储wchar_t 取值范围的类型:int、unsigned int、long或unsigned long。整型级别
简单地说,有符号整型按级别从高到低依次为long long、long、int、short和signed char。无符号整型的排列顺序与有符号整型相同。类型char、signed char和unsigned char的级别相同。类型bool的级别最低。wchar_t、char16_t和char32_t的级别与其底层类型相同。
ANSI C遵循的规则与ISO 2003 C++相同,这与前述规则稍有不同;而传统K&R C的规则又与ANSI C稍有不同。例如,传统C语言总是将float提升为double,即使两个操作数都是float。
强制类型转换
(long)thorn; //传统C
long(thorn); //C++
强制类型转换不会修改thorn变量本身,而是创建一个新的、指定类型的值,可以在表达式中使用这个值。
Stroustrup认为,C语言式的强制类型转换由于有过多的可能性而极其危险,于是在C++中引入了4个强制类型转换运算符,并对它们的使用严格限制,这将在之后的章节中详细介绍。在这4个运算符中,static_cast<>
可用于将值从一种数值类型转换为另一种数值类型:stati_cast<long>(thron)
C++11中的auto声明
auto n=100; //如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同
std::vector<double> scores;
std:vector<double>::iterator pv=scores.begin();
auto pv1=scores.begin(); //处理复杂类型,如标准模块库(STL)中的类型时,自动类型推断
在C语言中,
auto
关键字最初是为了指定变量的存储类别而设计的。然而,在现代C语言中,auto
实际上是局部变量的默认存储类别,因此在大多数情况下并不需要显式地使用它。而C++11为了让编译器能够根据初始值的类型推断变量的类型,于是其重新定义了auto
的含义。
复合类型
数组
数组之所以被称为复合类型,是因为它是使用其他类型来创建的(C语言使用术语“派生类型”,但由于C++对类关系使用术语“派生”,所以它必须创建一个新术语)。
C++标准模板库(STL)提供了一种数组替代品——模板类vector,而C++11新增了模板类array。这些替代品比内置复合类型数组更复杂、更灵活,本章将简要地讨论它们。
初始化方法
//传统C
int a[5] = { -1 }; //-1 0 0 0 0
int b[5] = {0}; //0 0 0 0 0
int c[] = { 1,2,3 }; //1 2 3
int elenum = sizeof c / sizeof(int); //3
//c++特性
int d[5]{-1}; //-1 0 0 0 0
int e[5]{}; //0 0 0 0 0
字符串
C++处理字符串的方式有两种。第一种来自C语言,常被称为C-风格字符串(C-style string),另一种基于string类库的方法。
//c-style
char a[3]={'H','i','\0'};
char b[3]="Hi"; //字符串字面量(常量),隐式地包括结尾的空字符。
char c[]="Hi";
//c++
std::string str1;
str1="hello"; //string对象不使用空字符来标记字符串末尾
std::cout<<str1[1]; //e
ISO/ANSI C++98标准通过添加
string
类扩展了C++库,因此现在能以string
类型的变量(使用C++的话说是对象)而不是字符数组来存储字符串。
string
类使用起来比数组简单,同时提供了将字符串作为一种数据类型的表示方法。
string
类定义隐藏了字符串的数组性质,让您能够像处理普通变量那样处理字符串。同时,可以使用数组表示法来访问存储在string
对象中的字符。
类设计让程序能够自动处理string
的大小。例如,string str1;
声明创建一个长度为0的string
对象,但程序str1="hello";
将自动调整str1
的长度。
拼接字符串常量
std::cout << "ni" "hao"; //nihao
std::cout << "ni"
"hao"; //nihao
事实上,任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。
其他操作
#include<iostream>
#include<cstring>
int main(void)
{
char ch1[10] = "hello";
char ch2[20];
std::string str1 = "hello";
std::string str2;
//拷贝
//std::strcpy(ch2, ch1);
strcpy_s(ch2,sizeof ch2, ch1);
str2 = str1;
//拼接
//std::strcat(ch2, ",world!");
strcat_s(ch2, sizeof ch2, ",world!");
str2 += ",world!";
std::cout << ch2 << " " << str2 << std::endl; //hello,world! hello,world!
//获取字符串长度
int len1 = std::strlen(ch2); //`strlen()`函数返回的是存储在数组中的字符串的长度,且只计算到第一个`\0`之前所有字符的个数。
int len2 = str2.size();
std::cout << len1 << " " << len2 << std::endl; //12 12
return 0;
}
在C++新增string类之前,程序员也需要完成诸如给字符串赋值等工作。对于C-风格字符串,程序员使用C语言库中的函数来完成这些任务。头文件
cstring
(以前为string.h
)提供了这些函数。例如,可以使用函数strcpy()
将字符串复制到字符数组中,使用函数strcat()
将字符串附加到字符数组末尾。
在较新的C/C++ 编译器中,strcpy()
和strcat()
被认为是有安全隐患的函数,因为它可能导致缓冲区溢出。为了避免这种问题,Microsoft Visual C++引入了安全版本的strcpy_s()
和strcat_s()
。
其他形式字符串字面量
wchar_t a[]=L"hello";
char16_t b[]=u"hello";
char32_t c[]=U"hello";
string
类单个字符只能是char
类型,与wchar_t
、char16_t
和char32_t
不能兼容使用。
原始字符串
char a[]= R"(haha\n)";
wchar_t b[]= LR"(haha\n)";
std::cout<<LR"(haha\n)"; //00007FF7C7EBAC28
std::string str1 = R"(haha\n)";
//自定义定界符
std::string str2 = R"*+(haha\n)")*+";
可将前缀R与其他字符串前缀结合使用(
LR
、uR
、UR
),以标识wchar_t
等类型的原始字符串。
原始字符串语法允许您在表示字符串开头的"和(之间添加其他字符,这意味着表示字符串结尾的"和)之间也必须包含这些字符(自定义定界符)。自定义定界符在默认定界符之间添加任意数量的基本字符,但空格、左括号、右括号、斜杠和控制字符(如制表符和换行符)除外。
结构
定义结构
struct struct_name
{
char name[20];
std::string name1;
int age;
double money;
};
声明结构变量
struct struct_name a; //传统c
struct_name b; //c++
C++允许在声明结构变量时省略关键字
struct
,这种变化强调的是,结构声明定义了一种新类型。
赋值
a={"hello","world",24,123.4};
b.name1 = "haha";
//拷贝
b = a;
std::cout << b.name1; //world
可以使用 赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值(memberwise assignment)。
定义+声明
struct struct_name
{
char name[20];
std::string name1;
int age;
double money;
}struct1,struct2;
//匿名结构
struct
{
char name[20];
std::string name1;
int age;
double money;
}struct3,struct4;
定义+声明+初始化
struct struct_name
{
char name[20];
std::string name1;
int age;
double money;
}struct1{"haha","nihao",23,123.4},struct2{};
成员函数
struct MyStruct {
int x;
int y;
// 成员函数
void setValues(int a, int b) {
x = a;
y = b;
}
// 另一个成员函数
int sum() const {
return x + y;
}
};
与C结构不同,C++结构除了成员变量之外,还可以有成员函数。但这些高级特性通常被用于类中,而不是结构中。
共用体
共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
union one4all
{
int int_val;
double double_val;
char id_val[20];
};
one4all a;
a.int_val = 15;
std::cout << a.int_val<<std::endl; //15
a.double_val = 1.2;
std::cout << a.double_val << std::endl; //1.2
std::cout << a.int_val << std::endl; //858993459 ,说明当double_val活跃时,int_val失效了,共用体只能同时存储一个成员的值。
共用体内的所有成员共享同一段内存空间,其必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度。
共用体不能与std::string
兼容使用。
共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。例如一些商品的ID为整数,而另一些的ID为字符串。struct widget { char brand[20]; int type; union id { long id_num; char id_char[20]; }id_val; }; //... widget a; if (a.type == 1) //程序员需要负责确认是哪个共用体成员在活跃 { std::cin >> a.id_val.id_num; } else { std::cin >> a.id_val.id_char; }
匿名共用体
struct widget
{
char brand[20];
int type;
union
{
long id_num;
char id_char[20];
};
};
//...
widget a;
if (a.type == 1)
{
std::cin >> a.id_num;
}
else
{
std::cin >> a.id_char;
}
结构/类中的匿名共用体,其成员同样被视为结构/类的成员。
枚举
定义枚举
enum spectrum { red, orange, yellow, green, blue, violet, indigo, ultraviolet};
让spectrum成为新类型的名称;spectrum被称为枚举(enumeration),就像struct变量被称为结构一样。
将red、orange、yellow等作为符号常量,它们对应整数值0~7。这些常量叫作枚举量(enumerator)。
在默认情况下,将整数值赋给枚举量,第一个枚举量的值为0,第 二个枚举量的值为1,依次类推。
声明枚举变量
spectrum band;
spectrum band=blue;
int color = 2 + band + orange;
std::cout << color; //7
spectrum band1=3; //invalid,int转enum需强制类型转换
在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量,像
++band;
都是非法的。
枚举量是整型,可被提升为int
类型,但int
类型不能自动转换为枚举类型。
匿名枚举
enum { red, orange, yellow, green, blue, violet, indigo, ultraviolet};
如果定义枚举只是为了定义符号常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。
设置枚举量的值
enum bits{one=1,two=2,four=4,eight=8};
enum bigstep{first,second=0,third=100,fourth}
可以使用赋值运算符来显式地设置枚举量的值,指定的值必须是整数(可为负数、可为long甚至long long类型的数)。
枚举,第一个枚举量的值默认是0,后面没有被初始化的枚举量的值将比其前面的枚举量大1。
可以创建多个值相同的枚举量。
枚举的取值范围
最初,对于枚举来说,只有声明中指出的那些值是有效的。然而,C++现在通过强制类型转换,增加了可赋给枚举变量的合法值。每个枚举都有取值范围,通过强制类型转换,可以将取值范围中的任何整数值赋给枚举变量,即使这个值不是枚举值。
取值范围的定义如下:首先,要找出上限,需要知道枚举量的最大值。找到大于这个最大值的、最小的2的幂,将它减去1,得到的便是取值范围的上限。例如,前面定义的bigstep
的最大值枚举值是101。在2的幂中,比这个数大的最小值为128,因此取值范围的上限为127。要计算下限,需要知道枚举量的最小值。如果它不小于0,则取值范围的下限为0;否则,采用与寻找上限方式相同的方式,但加上负号。例如,如果最小的枚举量为−6,而比它小的、最大的2的幂是−8(加上负号),因此下限为−7。
enum spectrum { red, orange, yellow, green, blue, violet, indigo, ultraviolet };
spectrum example1 = spectrum(6); //valid
spectrum example2 = spectrum(666); //理论上invalid
测试后发现,都没报错。。。
作用域内枚举
C++11扩展了枚举,增加了作用域内枚举(scoped enumeration).
enum spectrum { red, orange, yellow, green, blue, violet, indigo, ultraviolet};
std::cout<<green; //方式一
std::cout<<spectrum::green; //方式二
方式一尝试直接输出 green
而没有指定作用域。根据C++的标准,如果你在一个非枚举的作用域中直接使用枚举类型的成员,那么这个成员会被认为是一个未限定作用域的枚举成员。这意味着 green
会默认被解释为 spectrum::green
。但是,如果存在一个名为 green
的局部变量、函数或其他标识符的话,这种方式可能会导致编译错误。
方式二明确指定了枚举类型的作用域,即 spectrum::green
。这是推荐的做法,因为它可以避免任何潜在的名称冲突问题,并且清晰地表明你正在引用spectrum
枚举中的 green
成员。
指针
指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C语言中,可以用库函数malloc()
来分配内存;在C++中仍然可以这样做,但C++还有更好的方法——new
运算符。
int* ptr;
ptr = (int*)0xB8000000; //为指针赋明确地址
int* ptr1;
ptr1 = new int; //在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存,为数据提供空间是一个独立的步骤。
*ptr1 = 22;
std::cout << *ptr1; //22
delete ptr1; //这将释放ps指向的内存,但不会删除指针ps本身
指针与数组
int* a=new int;
*a=22;
delete a;
int* b=new int[10]; //使用new来创建动态数组
b[0]=0; //数组表示法
*(b+1)=1; //指针表示法
delete[] b; //释放数组,方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
- 将指针变量加1后,其增加的值等于指向的类型占用的字节数。
- 使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置;使用
new[]
运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。 - 可以把指针当作数组名使用,C和C++内部都使用指针来处理数组。数组和指针基本等价是C和C++的优点之一。
深入探究
探究一
int array1[10];
int* ptr1 = array1;
std::cout << array1 << " " << array1 + 1 << std::endl; //000000559DAFF618 000000559DAFF61C 相差一个int类型字节
std::cout << ptr1 << " " << ptr1 + 1 << std::endl; //000000559DAFF618 000000559DAFF61C 相差一个int类型字节
return 0;
始终牢记,对于指针而言,std::cout打印的是指针所指向的内存地址。
上述代码可以说明ptr1
与array1
等价吗?不可以,其只能说明数组名array1
存储了数组的第一个元素的地址,其是一个指针,且指向的是int
类型数据对象。
故array1
与ptr1
一样,都可使用数组表示法与指针表示法:
int array1[10];
int* ptr1 = array1;
//数组名
array1[0] = 100;
*(array1 + 1) = 200; //数组名使用指针表示法
//指针
*(ptr1 + 2) = 300;
ptr1[3] = 400; //指针使用数组表示法
std::cout << array1[0] << " " << array1[1] << " " << array1[2] << " " << array1[3] << std::endl; //100 200 300 400
探究二
那ptr1
与array1
究竟是否等价呢?答案是不等价:
int array1[10];
int* ptr1 = array1;
ptr1 = ptr1 + 1; //valid
array1 = array1 + 1; //invalid
编译器会报错,说表达式array1 = array1 + 1
必须是可修改的左值,编译器认为,array1
不可修改,即array1
是一个指针常量。指针常量的含义是:该类型指针,程序员不可以改变其指向, 但可以通过该指针改变其所指向的内存中所存储的值。
探究三
那么:
int array1[10];
int* const ptr1 = array1;
ptr1
与array1
又是否等价呢?答案是不等价:
int array1[10];
int* const ptr1 = array1;
std::cout << array1 << " " << array1 + 1 << std::endl; //000000F4B359F698 000000F4B359F69C 相差一个int类型字节
std::cout << ptr1 << " " << ptr1 + 1 << std::endl; //000000F4B359F698 000000F4B359F69C 相差一个int类型字节
std::cout << &array1 << " " << &array1 + 1 << std::endl; //000000F4B359F698 000000F4B359F6C0 相差整个数组字节
std::cout << &ptr1 << " " << &ptr1 + 1 << std::endl; //000000F4B359F6D8 000000F4B359F6E0 相差一个指针类型字节
ptr1
与array1
一样,都指向数组第一个元素的地址,指向的都是int
类型数据对象,且都是指针常量。
区别在于,array1
的地址,即&array1
,为000000F4B359F698
,它同样是数组第一个元素的地址,即array1
这个指针常量的地址与其所指向的内存的地址一样。诶,这是为什么呢?
&
是取址符,std::cout << &array1
打印的是array1
指针本身的地址,而不是指针所指向的内存地址。
探究四
很奇怪,为什么array1
这个指针常量的地址与其所指向的内存的地址一样,如果有C中多维数组的基础,应该会很好理解:
int array2[2][3] = { {1,2,3},{4,5,6} };
std::cout << &array2 << " " << &array2 + 1 << std::endl; //000000A86698FBC8 000000A86698FBE0 差6个int
std::cout << array2 <<" " <<array2+1<< std::endl; //000000A86698FBC8 000000A86698FBD4 差3个int
std::cout << *array2 << " " << *array2 + 1 << std::endl; //000000A86698FBC8 000000A86698FBCC 差1个int
std::cout << **array2 << std::endl; //1
array2
是一个二维数组名,它是一个二维指针,指向int[3]
类型,所以array2+1
增加了3个int
。
*array2
是一维指针,指向int类型,所以对*array2+1
增加了1个int
。
对于数组而言,解引用*
像是对array2
进行降维,对应的,取址符&
像是在对*array2
进行升维,升降维并不影响指针的指向,它仍是指向数组第一个元素的地址,只是改变了指向的类型。再次强调,这是对于数组而言。
基于此,应该就是能够理解,为何array2
和*array2
中存储的地址都是数组第一个元素的地址了。它们一个是数组名,一个是降维后的数组名。
那为什么&array2
也是数组第一个元素的地址?它貌似不再属于”对于数组而言“这样一个范畴了吧?
探究五
为什么&array2
也是数组第一个元素的地址?可以这样理解:
array2
为二维指针,指向的是数组的第一个元素地址;*array2
是对二维指针的降维,为一维指针,仍是指向数组的第一个元素地址。
从一维数组视角看,其实很难理解,为什么*array2
指向的是数组的第一个元素的地址,而*array2
的地址&*array2
也是数组第一个元素的地址。
但我们从二维数组视角看,&*array2
就是array2
,对于array2
而言,std::cout
输出的就是array2
所指向的地址,即数组第一个元素的地址。
如果说,&array2
属于”对于数组而言“这样一个范畴的话,那就十分合理了。那它是属于这个范畴吗?
探究六
array1究竟与什么等价?看如下代码:
int array1[10];
int(*ptr2)[10] = &array1;
std::cout << &array1 << " " << &array1 + 1 << std::endl; //0000005E4FB0F708 0000005E4FB0F730 相差整个数组字节
std::cout << ptr2 << " " << ptr2 + 1 << std::endl; //0000005E4FB0F708 0000005E4FB0F730 与 &array1等价
std::cout << array1 << " " << array1 + 1 << std::endl; //0000005E4FB0F708 0000005E4FB0F70C 相差一个int类型字节
std::cout << *ptr2 << " " << *ptr2 + 1 << std::endl; //0000005E4FB0F708 0000005E4FB0F70C 与 array1等价
(*ptr2)[1] = 200;
std::cout << array1[1] << std::endl; //200
可以这么理解,对于一维数组array1
,系统隐式创建了一个类型为int(*)[10]
的二维指针ptr2
,并将ptr2
对应的一维指针常量显式的开放为array1
数组名。
基于"探究四"可知,array1
也指向数组的第一个元素地址,ptr2
指向array1
,同时ptr2
也指向数组的第一个元素地址,说明,array1
所指向的内存地址和其本身的内存地址是一样的。我不清楚C里面是如何实现这一点的,我们记住就好了。
这时我们可以回应”探究五“中的问题了,&array2
确实属于”对于数组而言“这样一个范畴,n
维数组其实隐式创建了一个n+1
维的指针,&array2
仍是在这一范畴内。
探究七
sizeof返回数据对象本身所占的字节数,探究sizeof
与指针、数组的作用关系:
int array1[10];
int* ptr1 = array1;
int(*ptr2)[10] = &array1;
std::cout << sizeof array1 << std::endl; //40
std::cout << sizeof &array1 << std::endl; //8
std::cout << sizeof ptr1 << std::endl; //8
std::cout << sizeof &ptr1 << std::endl; //8
std::cout << sizeof ptr2 << std::endl; //8
std::cout << sizeof *ptr2 << std::endl; //40
//多维数组
int array2[2][3] = { {1,2,3},{4,5,6} };
std::cout << sizeof array2 << std::endl; //24
std::cout << sizeof * array2 << std::endl; //12
std::cout << sizeof ** array2 << std::endl; //4
std::cout << sizeof &**array2 << std::endl; //8
std::cout << sizeof &* array2 << std::endl; //8
std::cout << sizeof & array2 << std::endl; //8
ptr1
与ptr2
本身都是指针,sizeof
返回指针变量本身所占字节数,8字节。
所有最后带&
的,都代表地址值,sizeof
返回地址值所占字节数,8字节(地址值所占字节数=指针变量所占字节数)。
对数组应用sizeof
得到的是整个数组所占字节数,多维数组类比;对指针解引用后应用sizeof
得到的是指针指向的数据对象所占字节数。如果将数组名做参数传递给函数,那么在该函数中,数组名其实为与外部数组同名同地址的指针常量,对其应用sizeof
返回指针本身所占字节数。
int array2[2][3] = { {1,2,3},{4,5,6} }; std::cout << sizeof * array2 << std::endl; //12 std::cout << sizeof array2[0] << std::endl; //12 std::cout << sizeof ** array2 << std::endl; //4 std::cout << sizeof array2[0][0] << std::endl; //4
*
与[]
某种程度上而言,两者是等效的。
指针与字符串
数组和指针的特殊关系可以扩展到C-风格字符串。不可扩展到std::string,std::string
本质是个类,不再是某个特定类型的数组。
char a[10] = "aaaa";
const char* b = "bbbb";
std::cout << a << " " <<b<<" "<<"cccc"<<std::endl; //aaaa bbbb cccc
数组名是第一个元素的地址,因此std::cout
语句中的a是char
元素的地址。std::cout
对象认为char
的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符\0
为止。
在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。上述代码不会将整个字符串发送给std::cout
,而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。
在std::cout
和多数C++表达式中,char
数组名、char
指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
const char* a = "hahahaha";
std::cout << a << " " << (int*)a << std::endl; //hahahaha 00007FF63894BC30
一般来说,如果给std::cout
提供一个指针,它将打印地址。但如果指针的类型为char *
,则std::cout
将显示指向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型。
比较两c-style字符串时,不可使用
str1==str2
,该方式比较的是两地址,应该使用strcmp()
,而如果比较的两个字符串其中一个是string
类型,则可用==
,因为string
类对运算符进行了重载。
指针与结构
struct student
{
std::string name;
int age;
};
student* stuPtr1=new student;
(*stuPtr1).name="Mr.Crocodile";
stuPtr1->age=25;
new
和delete
使用规则
- 不要使用
delete
来释放不是new
分配的内存。 - 不要使用
delete
释放同一个内存块两次。 - 如果使用
new[ ]
为数组分配内存,则应使用delete[ ]
来释放。 - 如果使用
new
为一个实体分配内存,则应使用delete
(没有方括号)来释放。 - 对空指针应用
delete
是安全的。
数组的代替品
#include<iostream>
#include<vector>
#include<array>
int main(void)
{
//vector
std::vector<int>vi{1,2,3};
std::vector<int>vi1; //创建长度为0的vector
std::vector<int>vi2(6); //初始化为长度为6的vector
//array
std::array<int, 5> ai1 {1,2,3,4,5};
std::array<int, 5> ai2;
ai2 = ai1; //拷贝
//数组
int a[5]{ 1,2,3,4,5 };
//存储
std::cout << &vi << std::endl; //0000008CAF36F868 栈区
std::cout << &ai1 << std::endl; //0000008CAF36F8A8 栈区
std::cout << &a << std::endl; //0000008CAF36F908 栈区
std::cout << &vi[1]<<std::endl; //000002701B745F14 堆区
std::cout << &ai1[1] << std::endl; //0000008CAF36F8AC 栈区
std::cout << &a[1] << std::endl; //0000008CAF36F90C 栈区
//数组表示法
std::cout << vi[0] << std::endl; //1
std::cout << ai1[0] << std::endl; //1
std::cout << a[0] << std::endl; //1
//不检查是否越界,不安全
/*
vi[-2] = 100;
ai1[-2] = 200;
a[-2] = 300; // *(a-2)=300;
*/
//检查是否越界,安全
vi.at(2) = 5;
ai1.at(3) = 5;
return 0;
}
模板类vector
类似于string
类,也是一种动态数组。您可以在运行阶段设置vector
对象的长度,可在末尾附加新数据,还可在中间插入新数据。基本上,它是使用new
创建动态数组的替代品。实际上,vector
类确实使用new
和delete
来管理内存,但这种工作是自动完成的。
vector
和array
对象不禁止下标越界的行为,您仍可编写不安全的代码,然而,您还有其他选择。一种选择是使用成员函数at()
。中括号表示法和成员函数at()
的差别在于,使用at()
时,将在运行期间捕获非法索引,而程序默认将中断。这种额外检查的代价是运行时间更长,这就是C++让允许您使用任何一种表示法的原因所在。另外,这些类还让您能够降低意外超界错误的概率。例如,它们包含成员函数begin()
和end()
,让您能够确定边界,以免无意间超界,这将在之后的章节中讨论。
循环和关系表达式
for循环
for(int i=0;i<3;i++)
{
std::cout<<"haha"<<std::endl;
}
表达式和语句
int a, b, c;
a = b = c = 0;
std::cout.setf(std::ios_base::boolalpha);
std::cout << (a > b) << std::endl; //false
- 任何值或任何有效的值和运算符的组合都是表达式。表达式后面加上
;
即为表达式语句。 - C++将赋值表达式的值定义为左侧成员的值。
<<
运算符的优先级比表达式中使用的运算符高,因此代码使用括号来获得正确的运算顺序。
副作用和顺序点
副作用(side effect)指的是在计算表达 式时对某些东西(如存储在变量中的值)进行了修改;顺序点(sequence point)是程序执行过程中的一个点,在这里,进入下一步之前将确保对所有的副作用都进行了评估。在C++中,语句中的分号就是 一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。另外,任何完整的表达式末尾都是一个顺序点。
在C++11文档中,不再使用术语“顺序点”了,因为这个概念难以用于讨论多线程执行。相反,使用了术语“顺序”,它表示有些事件在其他事件前发生。这种描述方法并非要改变规则,而旨在更清晰地描述多线程编程。
逗号运算符
逗号运算符允许将两个表达式放到C++句法只允许放一个表达式的地方,并不是所有逗号都是运算符。如int a,b;
则是将逗号用于分开变量列表中的名称。
a++,b++;
c=(2,3,4); //c=4
- 逗号运算符从左往右计算
- 逗号运算符确保先计算完第一个表达式,然后计算第二个表达式(即逗号运算符是一个顺序点)。
- 最右边的值为逗号表达式的值。
- 在所有运算符中,逗号运算符的优先级是最低的。
while循环
int i=0;
while(i<5)
{
std::cout<<i<<std::endl;
i++;
}
do while循环
int i=0;
do
{
std::cout<<i<<std::endl;
i++;
}while(i<5);
基于范围的for循环(C++11)
int array[]={1,2,3,4,5};
for(int x:array) //遍历
{
std::cout<<x<<std::endl;
}
for(int &x:array) //引用
{
x*=2;
}
for(int x:{1,2,3,4}) //初始化列表
{
std::cout<<x<<std::endl;
}
- C++11新增了一种循环:基于范围(range-based)的
for
循环。这简化了一种常见的循环任务:对数组(或容器类,如vector
和array
)的每个元素执行相同的操作。 - 要修改数组的元素,需使用带
&
的循环变量语法。符号&
表明x
是一个引用变量,这个主题将在之后的章节中讨论。
分支语句和逻辑运算符
if语句
if(a==b)
{
std::cout<<"a==b"<<std::endl;
}
else if(a==c)
{
std::cout<<"a==c"<<std::endl;
}
else
{
std::cout<<"ok"<<std::endl;
}
逻辑表达式
运算符 | 另一种表示 |
---|---|
|| | or |
&& | and |
! | not |
&&
优先级比||
高- C++规定,
||
和&&
都是顺序点,因此在判定右侧前产生左侧表达式所有的副作用。
并不是所有的键盘都提供了用作逻辑运算符的符号,因此C++标准提供了另一种表示方式:and
、or
和not
。and
、or
和not
都是C++保留字,这意味着不能将它们用作变量名等,但它们不是关键字,因为它们都是已有语言特性的另一种表示方式。另外,它们并不是C语言中的保留字,但C语言程序可以将它们用作运算符,只要在程序中包含了头文件iso646.h
。C++不要求使用该头文件。
字符函数库cctype
函数 | |
---|---|
isalnum() | 如果参数是字母数字,即数字或者字母,该函数返回true |
isalpha() | 如果参数是字母,该函数返回true |
iscntrl() | 如果参数是控制字符,该函数返回true |
isdigit() | 如果参数是数字(0~9),该函数返回true |
isgraph() | 如果参数是除空格以外的打印字符,该函数返回true |
isslower() | 如果参数是小写字母,该函数返回true |
isprint() | 如果参数是打印字符(包括括号),该函数返回true |
ispunct() | 如果参数是标点符号,该函数返回true |
isspace() | 如果参数是标准空白字符,如:空格、进制、换行符、回车、水平制表符或者垂直制表符,该函数返回true |
isxdigit() | 如果参数是十六进制数字,即0-9、a-f或者A-F,该函数返回true |
tolower() | 如果参数是大写字符,则返回其小写,否则返回该参数 |
toupper() | 如果参数是小写字符,则返回其大写,否则返回该参数 |
条件运算符
int max = b>c ? b : c;
switch
enum STATUS{bad,normal,good};
STATUS status=STATUS::bad;
switch(status)
{
case STATUS::bad:
std::cout<<"status is bad"<<std::endl;
case STATUS::normal:
std::cout<<"status is not good"<<std::endl;
break;
case STATUS::good:
std::cout<<"status is good"<<std::endl;
break;
default:
std::cout<<"ok"<<std::endl;
}
枚举量作
switch
的case
标签,枚举量会被提升为int
类型,所以无法将值相等的两个枚举量都作case
标签。
goto
int main(void)
{
std::cout << "1" << std::endl;
std::cout << "2" << std::endl;
goto first;
std::cout << "3" << std::endl;
std::cout << "4" << std::endl;
first:
std::cout << "d" << std::endl;
std::cout << "dd" << std::endl;
return 0;
}
和C语言一样,C++也有goto语句。在大多数情况下(有些人认为,在任何情况下),使用goto语句不好,而应使用结构化控制语句(如if else
、switch
、continue
等)来控制程序的流程。
函数
基本知识
int sum(int a,int b); //函数原型
int main(void)
{
int a=sum(2,3); //函数调用
return 0;
}
int sum(int a,int b) //函数定义
{
return a+b;
}
C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象!有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。
C++原型与ANSI原型
ANSI C借鉴了C++中的原型,但这两种语言还是有区别的。其中最重要的区别是,为与基本C兼容,ANSI C中的原型是可选的,但在C++中,原型是必不可少的。在C++中,括号为空与在括号中使用关键字void是等效的——意味着函数没有参数。在ANSI C中,括号为空意味着不指出参数,在C++中,函数原型中不指定参数列表时应使用省略号。
void say_bye(...);
通常,仅当与接受可变参数的C函数(如printf()
)交互时才需要这样做。
函数指针
与数据项相似,函数也有地址,函数的地址是存储其机器语言代码的内存开始地址。
int sum(int a, int b);
int my_sum(int x, int y, int (*test_ptr)(int a, int b)); //my_sum()以函数指针作参数
int main(void)
{
int (*sum_ptr)(int a, int b); //函数指针,指定函数的返回类型和特征标(参数列表)
sum_ptr = sum; //函数名即为该函数的函数地址
int s=my_sum(2, 3, sum_ptr);
std::cout << s<<std::endl; //5
return 0;
}
int sum(int a, int b)
{
return a + b;
}
int my_sum(int x, int y, int (*test_ptr)(int a, int b))
{
int result = sum(x, y);
int result1 = (*test_ptr)(x, y); //使用指针来调用函数
int result2 = test_ptr(x, y); //test_ptr与(*test_ptr)等价
return result;
}
为何
pf
和(pf)
等价呢?一种学派认为,由于pf
是函数指针,而*pf
是函数,因此应将(*pf)()
用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf( )
用作函数调用使用。C++进行了折衷——这两种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。在认为这种折衷粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特点。
函数指针数组
int sum1(int a,int b);
int sum2(int a,int b);
int sum3(int a,int b);
int (*ptr_array[3])(int a,int b){sum1,sum2,sum3}; //auto自动类型推断只能用于单值初始化,而不能用于初始化列表。
auto ptr_array_ptr=&ptr_array; //指向函数数组的指针
(*ptr_array_ptr)[1](2,3); //==> sum2(2,3);
函数探幽
内联函数
#include<iostream>
inline int sum(int a, int b)
{
return a + b;
}
int main(void)
{
int a=sum(2, 3);
std::cout << a << std::endl;
return 0;
}
-
执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
-
在函数声明前加上关键字
inline
;在函数定义前加上关键字inline
。通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。尽管程序没有提供独立的原型,但C++原型特性仍在起作用。这是因为在函数首次使用前出现的整个函数定义充当了原型。 -
内联函数和常规函数一样,也是按值来传递参数的。内联函数不能递归。
引用变量
#include<iostream>
void swap(int& a, int& b);
int main(void)
{
int rats=6;
int& rodents = rats;
std::cout << &rats <<" "<<rats<< std::endl; //0000007D4EEFFC64 6
std::cout << &rodents << " " << rodents << std::endl; //0000007D4EEFFC64 6
int a = 2;
int b = 3;
swap(a, b);
std::cout << a << " " << b << std::endl; //3 2
return 0;
}
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
-
C++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。
-
rodents
是rats
的引用,它们指向相同的值和内存单元。int&
指的是指向int
的引用,其中,&
不是地址运算符,而是类型标识符的一部分。 -
引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。
-
区别于指针,必须在声明引用变量时进行初始化。
int& rodents = rats;
等效于int* const rodents=&rats;
。 -
当形参引用为
const
,且实参的类型正确但不是左值时,或实参的类型不正确但可以转换为正确的类型时,C++将生成一个临时匿名变量,并让引用变量指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。若形参引用不为const
,则不可自动创建临时匿名变量,编译器报错。 -
C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用
&&
声明的。int&& ref1=15; int a=2; int&& ref2=2*a+3;
新增右值引用的主要目的是,让库设计人员能够提供某些操作的更有效实现。在之后的章节中将讨论如何使用右值引用来实现移动语义。以前的引用(使用
&
声明的引用)现在称为左值引用。
返回引用类型
#include<iostream>
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void display(const free_throws& ft);
void set_pc(free_throws& ft);
free_throws& accumulate(free_throws& target, const free_throws& source);
int main(void)
{
free_throws one = { "Ifelsa Branch",13,14 };
free_throws two = { "Andor Knott",10,16 };
free_throws three = { "Minnie Max",7,9 };
free_throws team = { "Throwgoods",0,0 };
free_throws dup;
set_pc(one);
display(one);
accumulate(team, one);
display(accumulate(team, two));
dup = accumulate(team, three);
return 0;
}
void display(const free_throws& ft)
{
std::cout << "Name:" << ft.name << std::endl;
std::cout << "Made:" << ft.made << std::endl;
std::cout << "Attempts:" << ft.attempts << std::endl;
std::cout << "Percent:" << ft.percent << std::endl;
}
void set_pc(free_throws& ft)
{
if (ft.attempts != 0)
ft.percent = 100.0 * float(ft.made) / float(ft.attempts);
else
ft.percent = 0;
}
free_throws& accumulate(free_throws& target, const free_throws& source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
accumulate()
返回类型为free_throws&
,dup = accumulate(team, three);
则是将target
的值直接拷贝给dup
。若accumulate()
返回类型为free_throws
,dup = accumulate(team, three);
则是将target
的值先复制到一个临时位置,然后调用程序再将这个临时位置上的值拷贝给dup
。返回引用类型效率更高。
返回引用时最重要的一点是,应避免被调用函数终止时不再存在的内存单元引用,如返回一个局部变量,局部变量在函数运行完毕后将不再存在。为避免这种问题,最简单的方法是返回一个作为参数传递给函数的引用(如accumulate()
所做的),另一种方法是用new
来分配新的存储空间(使用完记得delete
)。
free_throws& clone(const free_throws& source)
{
free_throws* copy=new free_throws;
*copy=source;
return *copy;
}
free_throws& jolly=clone(three);
必须用
free_throws&
接收clone(three)
的返回值,若使用free_throws
,一方面会导致再复制一遍,更重要的是会造成内存泄漏。
类对象和引用
基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
何时使用引用参数
- 如果数据对象很小,且不对其进行修改,则按值传递;若要对其修改,则使用指针或引用。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是较大的结构,可使用指针或引用。
- 如果数据对象是类对象,则使用引用。
类设计的语义常常要求使用引用,这是C++新增引用特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
默认参数
int sum(int a, int b=1);
int main(void)
{
int a=2;
int result1=sum(2,3);
int result2=sum(a);
}
int sum(int a, int b)
{
return a+b;
}
- 只能通过函数原型对函数参数设置默认值,函数定义不变。
- 对于带参数列表的函数,必须从右向左添加默认值。
函数重载
默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。C++允许定义名称相同的函数,条件是它们的特征标不同。
void stove(double& r1); //#1
void stove(const double& r2); //#2
void stove(double&& r3); //#3
左值引用参数r1
与可修改的左值参数(如double
变量)匹配;const
左值引用参数r2
与可修改的左值参数、const
左值参数和右值参数(如两个double
值的和)匹配;最后,右值引用参数r3
与右值匹配。注意到与r1
或r3
匹配的参数都与r2
匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本:
double x=55.5;
const double y=32.0;
stove(x); //#1
stove(y); //#2
stove(x+y); //#3
- 最好不要重载一个带默认参数的函数。
- 编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
- C++将尝试使用标准类型转换强制进行匹配。
- 匹配函数时,区分
const
和非const
变量。
名称修饰
C++编译器通过名称修饰(namedecoration)/ 名称矫正(name mangling)跟踪每一个重载函数,它根据函数原型中指定的形参类型对每个函数名进行加密。如未经修饰的函数原型:long (int,float);
,这种格式对于人类来说很适合,而编译器将名称转换为不太好看的内部表示来描述该接口:?MyFunctionFoo@@YAXH
,添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异。
函数模板
#include<iostream>
template<typename T>
void Swap(T& a, T& b);
int main()
{
int a = 2, b = 3;
Swap(a, b);
double c = 2.0, d = 3.0;
Swap(c, d);
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int
或double
)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类(parameterized types)。
要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用Swap()
函数即可。编译器将检查所使用的参数类型,并生成相应的函数。
模板并不创建任何函数,而只是告诉编译器如何定义函数。函数模板也不能缩短可执行程序,当程序用到了int
版的Swap()
和double
版的Swap()
,编译器会自动定义相应版本的函数,最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。
在标准C++98添加关键字typename
之前,C++使用关键字class
来创建模板。也就是说,可以这样编写模板定义:
template<class T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
typename
关键字使得参数T
表示类型这一点更为明显;然而,有大量代码库是使用关键字class
开发的。在这种上下文中,这两个关键字是等价的。
模板的重载
#include<iostream>
template<typename T>
void Swap(T& a, T& b);
template<typename T>
void Swap(T* a, T* b,int len); //重载
int main()
{
const int len = 6;
int a[len]{ 1,2,3,4,5,6 };
int b[len]{ 10,20,30,40,50,60 };
Swap(a, b,len);
std::cout << e[1] << std::endl; //20
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
template<typename T>
void Swap(T* a, T* b, int len)
{
for (int i = 0; i < len; i++)
{
T temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
显式具体化
#include<iostream>
struct job
{
std::string name;
int salary;
};
void Swap(int& a,int& b); // #0
template<typename T> // #1
void Swap(T& a, T& b);
template<> void Swap<job>(job& a, job& b); // #2 <job>可省,因为函数的参数类型表明这是job的一个具体化
int main()
{
job a{ "driver",8000 };
job b{ "cooker",9000 };
Swap(a, b);
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
template<> void Swap<job>(job& a, job& b) //<job>可省
{
int temp = a.salary;
a.salary = b.salary;
b.salary = temp;
}
void Swap(int& a,int& b)
{
int temp=a;
a=b;
b
}
#0
为非模板函数,#2
是#1
的具体化,匹配优先级:#0
>#2
>#1
显式实例化
在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。
最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化(explicit instantiation)。实例化后程序中将包含函数定义。
#include<iostream>
struct job
{
std::string name;
int salary;
};
template<typename T>
void Swap(T& a, T& b); //#1
template<typename T>
void Swap(T* a, T* b, int len); //#2 模板重载
template<> void Swap(job& a, job& b); //#3 显式具体化
template void Swap<int>(int&, int&); //#4 显式实例化
int main()
{
int a = 2;
int b = 3;
Swap(a, b); //匹配#4
return 0;
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
template<typename T>
void Swap(T* a, T* b, int len)
{
for (int i = 0; i < len; i++)
{
T temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
template<> void Swap(job& a, job& b)
{
int temp = a.salary;
a.salary = b.salary;
b.salary = temp;
}
在C++中,模板的显式实例化是一种机制,允许你在编译时强制生成特定类型的模板实例。这通常用于确保某些关键函数或类模板的具体实例在链接时可用,尤其是在这些实例需要放置在不同的编译单元中时。
先具体化再实例化,编译器不会报错,但如果先实例化再具体化,则会报"已实例化"错误信息。
显式实例化位置
函数外
template<typename T>
void Swap(T& a, T& b);
template void Swap<int>(int&, int&); //显式实例化
int main()
{
double a = 2.0;
double b = 3.0;
Swap(a, b);
return 0;
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
模板类外
// MyClass.h
#pragma once
#include <iostream>
// 模板类声明和实现都放在头文件中
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T value);
void display();
};
// 构造函数的实现
template <typename T>
MyClass<T>::MyClass(T value) : data(value) {}
// 成员函数的实现
template <typename T>
void MyClass<T>::display() {
std::cout << "Data: " << data << std::endl;
}
template class MyClass<int>; //显式实例化
最常见的做法是将模板类的声明和实现都放在头文件中,方便模板实例化。这样组织可以保持代码的可读性,同时确保编译器在需要时能够找到模板类的实现,避免出现链接错误。
名称空间内
// MyNamespace.h
#pragma once
#include <iostream>
namespace MyNamespace {
template <typename T>
void printValue(T value);
}
// MyNamespace.hpp
#include "MyNamespace.h"
namespace MyNamespace {
template <typename T>
void printValue(T value) {
std::cout << "Value: " << value << std::endl;
}
template void printValue<int>(int); //显式实例化
}
编译器选择使用哪个函数版本
重载解析
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。
重载解析步骤:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 匹配特征标,可隐式类型转换。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
何为最佳:
- 完全匹配,但常规函数优先于模板。
- 提升转换(例如,
char
和shorts
自动转换为int
,float
自动转换为double
)。 - 标准转换(例如,
int
转换为char
,long
转换为double
)。 - 用户定义的转换,如类声明中定义的转换。
完全匹配允许的无关紧要转换:
从实参 | 到形参 |
---|---|
T | T& |
T& | T |
T[] | *T |
T(argument-list) | T(*)(argument-list) |
T | const T |
T | volatile T |
T* | const T |
T* | volatile T* |
然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非const
数据的指针和引用优先与非const
指针和引用参数匹配。const
和非const
之间的区别只适用于指针和引用指向的数据。
一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。术语"最具体(most specialized)"并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。
template <typename T> void recycle(T t); //#1
template <typename T> void recycle(T *t); //#2
struct blot
{
int a;
char b[10];
};
int main()
{
blot ink={25,"sports"};
recycle(&ink);
return 0;
}
recycle(&ink)
调用与#1
模板匹配时将T
解释为blot*
,与#2
模板匹配时将T
解释为blot
。则候选函数列表中有void recycle<blot*> (blot* t);
和void recycle<blot> (blot* t);
,其中,void recycle<blot> (blot* t);
被认为更具体,所以会调用#2
,因为#2
参数被更具体为指向某类型的指针。
用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。和显式实例一样,这也是C++98新增的特性,这里不做过多介绍。
简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数;如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的。当然,如果不存在匹配的函数,则也是错误。
程序员指定
在有些情况下,可通过编写合适的函数调用,引导编译器做出您希望的选择。
#include<iostream>
void Swap(int& a,int& b); //#0
template<typename T>
void Swap(T& a, T& b); //#1
int main()
{
int a = 2;
int b = 3;
Swap(a, b); //按编译器重载解析原则,匹配 #0
Swap<>(a, b); //<>指出编译器应选择模板函数,按编译器重载解析原则,匹配int版的#1
Swap<long>(a, b); //<long>指出编译器应选择模板函数,匹配long版的#1
return 0;
}
void Swap(int& a,int& b)
{
int temp=a;
a=b;
b
}
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
模板函数的发展
decltype
//c++98
template<typename T1,typename T2>
void ft(T1 x,T2 y)
{
//...
?TYPE? xpy =x+y;
//...
}
在C++98中没有办法声明xpy
的类型,C++11新增的关键字decltype
提供了解决方案:
//c++11
template<typename T1,typename T2>
void ft(T1 x,T2 y)
{
//...
decltype(x+y) xpy =x+y;
//...
}
decltype
与typedef
组合使用://c++11 template<typename T1,typename T2> void ft(T1 x,T2 y) { //... typedef decltype(x+y) type1; type1 xpy =x+y; //... }
decltype(expression) var;
long sum(int,int);
int a=10;
decltype(a) var1; //int
decltype(sum(2,3)) var2; //long 编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
decltype((a)) var3; //int&
- 如果
expression
是一个没有用括号括起的标识符,则var
的类型与该标识符的类型相同,包括const
等限定符。 - 如果
expression
是一个函数调用,则var
的类型与函数的返回类型相同。 - 如果
expression
是用括号括起的标识符,则var
为指向其类型的引用。
后置返回类型
有一个相关的问题是decltype
本身无法解决的:
template<typename T1,typename T2>
?TYPE? ft(T1 x,T2 y)
{
//...
return x+y;
}
无法预先知道将x
和y
相加得到的类型,且由于参数x
和y
未声明,无法使用decltype
。为此,C++新增了一种声明和定义函数的语法(后置返回类型):
//声明
auto sum(int,int) -> int;
template<typename T1,typename T2>
auto ft(T1 x,T2 y) -> decltype(x+y);
//定义
template<typename T1,typename T2>
auto ft(T1 x,T2 y) -> decltype(x+y)
{
//...
return x+y;
}
内存模型和名称空间
单独编译
虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++标准使用了术语"翻译单元(translation unit)“,因为文件并不是计算机组织信息时的唯一方式。出于简化的目的,本文使用术语"文件”,您可将其解释为翻译单元。
头文件内容
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 内联函数
- 函数声明
- 模板声明
同一个文件中只能将同一个头文件包含一次,否则,可能在一个文件中定义同一个结构两次,这将导致编译错误。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef
。
#ifndef SETTING_H
#define SETTING_H
//...
#endif
多个库的链接
C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此由不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。也就是说,两个编译器将为同一个函数生成不同的修饰名称。名称的不同将使链接器无法将一个编译器生成的函数调用与另一个编译器生成的函数定义匹配。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。
存储持续性、作用域和链接性
持续性描述数据保留在内存中的时间。作用域(scope)描述名称在翻译单元的多大范围内可见。链接性(linkage)描述名称如何在不同翻译单元间共享。
存储方案 | 存储持续性 |
---|---|
自动存储 | 在执行完代码块后自动释放 |
静态存储 | 在程序整个运行过程中都存在 |
线程存储 | 其生命周期与所属的线程一样长 |
动态存储 | 用new分配的内存将一直存在,直到使用delete释放或程序结束为止。 |
存 储 描 述 | 持 续 性 | 作 用 域 | 链 接 性 | 如 何 声 明 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
static
用于局部声明,以指出变量是无链接性的静态变量时,static
表示的是存储持续性;而用于外部声明时,static
表示内部链接性。有人称之为关键字重载,即关键字的含义取决于上下文。
自动存储
int main()
{
int a; //普通自动存储变量
register int b; //寄存器变量
return 0;
}
关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量,这旨在提高访问变量的速度。
在C++11之前,register
在C++中的用法始终未变,其表明该变量经常被访问,编译器可对其做特殊处理以加快访问速度。但在C++11中,register
失去了原先的作用,其只是显式地指出变量是自动的,这与auto
之前的用途完全相同。目前仍保留该关键字的重要原因是避免使用了该关键字的现有代码非法。
静态存储
初始化
#include<cmath>
int main()
{
static a; //零初始化,未被初始化的静态变量的所有位都被设置为零
static b=2*3; //零初始化->常量表达式初始化
static c=sizeof(int); //零初始化->常量表达式初始化
static d=atan(1); //零初始化->动态初始化
return 0;
}
-
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。
-
常量表达式并非只能是使用字面常量的算术表达式,例如,它还可使用
sizeof
运算符。 -
所有静态变量都先会被零初始化;接下来如果使用常量表达式初始化了变量,且编译器仅根据文件内容(包括被包含的头文件)就可计算表达式,编译器将执行常量表达式初始化;如果没有足够的信息,变量将被动态初始化。
示例中只是包含了
cmath
头文件,并没有atan()
定义,需要等该函数被链接且程序执行时才能初始化,故为动态初始化。 -
C++11新增了关键字
constexpr
,这增加了创建常量表达式的方式,在此不多做介绍。
传统的K&R C只允许初始化静态数组和结构,不允许初始化自动数组和结构,ANSI C和C++允许对这两种数组和结构进行初始化。有些旧的C++编译器与ANSI C不完全兼容,如果使用的是这样的实现,则可能需要使用静态存储类型以初始化数组和结构。
外部变量+外部链接性
链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件(也称全局变量)。
//setting.cpp
bool RUNNING=true; //定义声明
//...
//runThread.cpp
extern bool RUNNING; //引用声明
//...
更常规做法:
//setting.h
namespace STATE
{
extern bool RUNNING; //引用声明
}
//setting.cpp
#include"setting.h"
namespace STATE
{
bool RUNNING=true; //定义声明
}
//runThread.h
#include<iostream>
#include"setting.h" //引用包含要用的外部变量的引用声明的头文件
void show();
//...
//runThread.cpp
#include"runThread.h"
void show()
{
extern bool STATE::RUNNING; //重复引用申明外部变量,可省略
std::cout<<STATE::RUNNING<<std::endl;
}
//...
- 单定义规则(One Definition Rule,ODR):变量只能有一次定义。
- 文件中必须声明所使用的外部变量。
- 为满足以上两点,C++提供了两种变量声明方式。一种是定义声明(defining declaration),它给变量分配存储空间;另一种是引用声明(referencing declaration),它不给变量分配存储空间。
- 引用声明使用关键字
extern
,且不进行初始化,否则声明为定义,导致分配存储空间。
作用域解析运算符
int STATE=1;
int main()
{
std::cout<<"start"<<std::endl;
std::cout<<STATE<<std::endl; //1
int STATE=2;
std::cout<<STATE<<std::endl; //2 局部变量,全局变量被隐藏
std::cout<<::STATE<<std::endl; //1 使用C++的作用域解析运算符来访问被隐藏的外部变量
return 0;
}
外部变量+内部链接性
//setting.cpp
bool RUNNING=true; //常规外部变量,具有外部链接性,可多文件中使用
//...
//runThread.cpp
static bool RUNNING=true; //特殊外部变量,具有内部链接性,会隐藏同名常规外部变量,不违反单定义规则。
//...
局部变量(无链接性)
long sumAll(int a)
{
static long total;
total+=a;
return total;
}
- 在代码块中使用
static
时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。 - 静态局部变量的声明及初始化只会在代码块被第一次调用时启用,以后再调用函数时,将不会像自动变量那样再次被声明和初始化。
限定符
const
- 在C++(但不是在C语言)中,
const
限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const
全局变量的链接性为内部的,像是使用了static
说明符一样。这也是能够将常量定义放在头文件中的原因。 - 如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用
extern
关键字来覆盖默认的内部链接性:extern const int state=50;
volatile
关键字volatile
表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。其应用点在于,程序可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。硬件(而不是程序)可能修改其中的内容,程序要读取硬件变动后的值。但编译器发现,程序在几条语句中多次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是自动优化为将这个值缓存到寄存器中,多次读取寄存器中的值,非目标功能。将变量声明为volatile
,相当于告诉编译器不要进行这种优化,每次读取该变量的值都得去重新访问该变量地址。
mutable
struct Data
{
std::string name;
mutable bool accesses;
}
int main()
{
const Data d1={"Tom",true};
d1.accesses=false; //mutable特性
return 0;
}
即使结构(或类)变量为const
,其某个成员也可以被修改。
函数和链接性
extern int sum(int,int);
static int sum1(int,int);
static int sum1(int a,int b)
{
return a+b;
}
- 可以在函数原型中使用关键字
extern
来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。 - 关键字
static
是的函数的链接性设置为内部,使之只能在一个文件中使用。必须在原型和定义中同时使用该关键字。 - 内联函数不受单定义规则的约束,这使得程序员能将内联函数的定义放在头文件中。然而,C++要求同一个函数的所有内联定义都必须相同。
语言链接性
链接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,C语言编译器可能将spiff(int)
这样的函数名翻译为_spiff
,函数调用时约定查找_spiff
对应的机器代码。在C++中,C++编译器需执行名称修饰为重载函数生成不同的符号名称,其可能将spiff(int)
的函数名翻译为_spiff_i
,函数调用时约定查找_spiff_i
对应的机器代码。因此如果要在C++程序中使用C库中预编译的函数,由于两者约定不同会导致调用失败。解决这种问题,可以用函数原型来指出要使用的约定:
extern "C" void spiff1(int); //C链接性(C约定)
extern "C++" void spiff2(int); //C++链接性(C++约定)
extern void spiff3(int); //默认C++链接性
C和C++链接性是C++标准指定的说明符,但实现可提供其他语言链接性说明符。
动态存储
int main()
{
int* ptr=new int[3]; //ptr指针变量为自动存储,但分配的3个int内存为动态存储
return 0;
}
- 由
new
分配的内存将一直被占用,直到delete
释放或者程序结束。 new
可能找不到请求的内存量。在最初的10年中,C++在这种情况下让new
返回空指针,但现在将引发异常std::bad_alloc
。
在程序结束时,由new分配的内存通常都将被释放,不过情况也并不总是这样。例如,在不那么健壮的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放。最佳的做法是,使用delete来释放new分配的内存。
定位new
运算符
new
负责在堆(heap)中找到一个足以能够满足要求的内存块,定位new
运算符则能够让您指定要使用的内存地址。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
#include<iostream>
#include<cmath>
#include<new>
const int BUF = 10;
const int N = 5;
int main()
{
char buffer[BUF];
double* ptr1 = new(buffer) double[N];
std::cout << buffer<<std::endl; //烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫桷y劸
std::cout << (void*)buffer << std::endl; //000000BE8479F6E8
std::cout << ptr1 << std::endl; //000000BE8479F6E8
return 0;
}
- 定位
new
运算符工作原理:只是返回传递给它的地址,并将其强制转换为void*
,以便能够赋给任何指针类型。 p2
和buffer
的地址,但类型不同。- 定位
new
运算符使用传递给它的地址,它不关注内存单元是否被占用,其将一些内存管理方面的责任交给了程序员。 - 由
new
运算符分配的动态内存需要用delete
释放,如上文定位new
运算符指向的是buffer
分配的栈内存,则不需要用delete释放。
运算符、替换函数
int* ptr1=new int; //运算符重载,激活调用#1
int* ptr11=new(sizeof(int)); // #1
int *ptr2=new int[3]; //运算符重载,激活调用#2
int* ptr22=new(3*sizeof(int)); // #2
delete ptr1; //运算符重载,激活调用#3
delete(ptr1); // #3
delete[] ptr2; //运算符重载,激活调用#4
delete[](ptr22); // #4
#1
、#2
这些被称为分配函数,#3
、#4
这些被称为释放函数。C++将这些函数称为可替换的(replaceable)。这意味着如果您有足够的知识和意愿,可为new
运算符和delete
运算符提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new
运算符,但它将调用您定义的new()
函数。
int* ptr3=new(buffer) int; //运算符重载,激活调用#5
int* ptr33=new(sizeof(int),buffer); // #5
int *ptr4=new(buffer) int[3]; //运算符重载,激活调用#6
int* ptr44=new(3*sizeof(int),buffer); // #6
定位new
函数不可替换,但可重载。它至少需要接收两个参数,其中第一个参数类型总是std::size_t
,其指定了请求的字节数。这样的重载函数都被称为定位new
,即使额外的参数没有指定目标位置。
名称空间
namespace
//setting.h
namespace Setting
{
const int MOUTHS=12; //内部链接性
int count; //外部链接性
int sum(int,int);
}
//setting.cpp
#include"setting.h"
namespace Setting
{
int count=0;
int sum(int a,int b)
{
return a+b;
}
}
//main.cpp
#include<iostream>
#include"setting.h"
int main()
{
std::cout<<Setting::MOUTHS<<std::endl; //12
std::cout<<Setting::sum(2,3)<<std::endl; //5
}
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此在默认情况下,在名称空间中声明的名称有外部链接性(除非它使用
const
限定)。 count
为未限定的名称,Setting::count
为限定的名称。
using
namespace Try
{
int count = 12;
}
int main(void)
{
int count = 1;
std::cout << count << std::endl; //1
using namespace Try; //using编译指令
std::cout << count << std::endl; //1
return 0;
}
namespace Try
{
int count = 12;
}
int main(void)
{
using namespace Try;
std::cout << count << std::endl; //12
int count = 1;
std::cout << count << std::endl; //1
return 0;
}
namespace Try
{
int count = 12;
int sum(int a,int b)
{
return a+b;
}
}
int main(void)
{
using Try::count; //using声明
using Try::sum; //using声明函数,会导入函数所有重载版本
int count = 1;
std::cout << count << std::endl; //报错 Try::count重定义;多次初始化
return 0;
}
假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
嵌套
namespace Setting1 //命名空间嵌套
{
int count = 12;
namespace Setting2
{
int count = 1;
}
}
int main(void)
{
using namespace Setting1;
std::cout << count << std::endl; //12
std::cout << Setting2::count << std::endl; //1
return 0;
}
namespace Setting1
{
int count = 12;
namespace Setting2
{
int count = 1;
}
}
int main(void)
{
using namespace Setting1::Setting2; //声明嵌套的命名空间
std::cout << count << std::endl; //1
return 0;
}
别名
namespace Setting1
{
int count = 12;
namespace Setting2
{
int count = 1;
}
}
int main(void)
{
namespace Setting3=Setting1::Setting2; //命名空间别名
using namespace Setting3;
std::cout << count << std::endl; //1
return 0;
}
匿名
namespace //匿名命名空间 全局+内部链接性,等价于外部static int count = 12;
{
int count = 12;
}
int main(void)
{
std::cout << count << std::endl; //12
return 0;
}
提供了链接性为内部的静态变量的替代品。
其他
类型别名
C++为类型创建别名的方式有两种。一种是使用预处理器,一种是使用关键字typedef
。
#define INT int;
上述方式都是为int
设置别名INT
.但#define
有其局限:
#define INTPOINT int*;
INTPOINT a,b; //==> int *a,b;
typedef int* INTPOINT;
INTPOINT a,b; //==> int *a, *b;
typedef
创建类型别名还有另一用法:将别名当做标识符进行声明,并在开头使用关键字typedef
。(其实该思想是与typedef int INT;
相兼容的)
typedef int (*ptrA)[3];
ptrA ptr1; //ptr1为指向包含3个int类型数值的数组的指针
typedef
能够处理更复杂的类型别名,为类型创建别名推荐使用typedef
。
宏
定义
在C++中,宏(macro)是一种预处理器指令,它允许你在编译之前将代码片段替换为其他代码片段。宏定义通常通过#define
指令来创建,并且在代码编译前由预处理器进行替换。
类型
- 常量宏:
#define PI 3.14159
- 函数式宏 :
#define SQUARE(x) ((x)*(x))
需要注意的问题
- 括号问题
- 副作用:宏没有函数调用的语义,因此当宏参数有副作用时需要小心。例如,
#define MAX(a, b) ((a) > (b) ? (a) : (b))
如果用在表达式中如MAX(i++, 10)
,那么i++
会被计算两次,导致不确定的行为。
由于上述原因,现代C++编程倾向于使用内联函数(inline functions)或者模板(templates)来代替宏,因为它们提供了更好的类型检查和错误检测机制。
标签:std,函数,int,void,C++,第六版,Plus,类型,Primer From: https://blog.csdn.net/weixin_45949932/article/details/142281238