首页 > 编程语言 >C++ Primer Plus 第六版中文版(上)

C++ Primer Plus 第六版中文版(上)

时间:2024-09-15 11:49:40浏览次数:3  
标签:std 函数 int void C++ 第六版 Plus 类型 Primer

参考资料:《C++ Primer Plus 第六版中文版》
笔记作者:Mr.Crocodile
欢迎转载

文章目录

开始学习C++

头文件命名规定

头文件类型约定示例说明
C旧风格.h结尾math.hCC++
C++旧风格.h结尾iostream.hC++
C新风格加上前缀c,没有扩展名cmathC++
C++新风格没有扩展名iostreamC++

名称空间

//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

coutcin

在这里插入图片描述

#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++提供了两个用于处理输入和输出的预定义对象:coutcin,它们是istreamostream类的实例,这两个类是在iostream文件中定义的。为ostream类定义的插入运算符<<使得将数据插入输出流成为可能;为istream类定义的抽取运算符>>能够从输入流中抽取信息。coutcin都是智能对象,能够根据程序上下文自动将信息从一种形式转换成另一种形式。
  • C++提供两种发送消息的方式:一种方式是使用类方法,另一种方式是重新定义运算符(运算符重载),coutcin使用的就是这种方式。

函数

  • 和C一样,C++不允许函数定义嵌套,每个函数定义都是独立的,所有函数的创建都是平等的。
  • C++可以使用大量库函数,库函数的编译代码包含在库文件中,编译器编译程序时,它必须在库文件搜索您使用的函数,至于自动搜索哪些库文件,将因编译器而异。
  • 使用库函数必须提供函数原型,有两种方式提供:一是在源代码文件中输入函数原型;二是包含相关头文件,其中定义了该库函数的函数原型。(用方式二)

处理数据

简单变量

变量命名规则

  • 在名称中只能使用字母、数字和下划线。
  • 名称的第一个字符不能是数字。
  • 字母区分大小写。
  • 不能将C++关键字用作名称。
  • 以两个下划线打头,或以一个下划线和一个大写字母打头的标识符是被保留给实现(编译器、库等)使用的;以一个下划线打头的标识符作全局标识符时是被保留给实现(编译器、库等)使用的。
  • C++对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。

最后一点使得C++与ANSI C(C99标准)有所区别,后者只保证名称中的前63个字符有意义(在ANSI C中,前63个字符相同的名称被认为是相同的,即使第64个字符不同)。
如果想用两个或更多的单词组成一个名称,通常的做法是用下划线将单词分开(C语言方式),或者从第二个单词开始将每个单词的第一个字母大写(Pascal语言方式)。

整型

C++的基本整型(按宽度递增的顺序排列)分别是charshortintlong和C++11新增的long long,其中每种类型都有符号版本和无符号版本,因此总共有10种类型可供选择。由于char类型有一些特殊属性(它最常用来表示字符,而不是数字),因此本章将首先介绍其他类型。

C++的shortintlonglong 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_MAXint的最大取值,CHAR_BIT为字节的位数。

  • 可对类型名或变量名使用sizeof运算符。对类型名使用sizeof运算符时,应将名称放在括号中;但对变量名使用该运算符,括号是可选的。
  • 头文件climits定义了符号常量来表示类型的限制。编译器厂商提供了climits文件,该文件指出了其编译器中的值。

climits中的符号常量

符号常量表示
CHAR_BITchar的位数
CHAR_MAXchar的最大值
CHAR_MINchar的最小值
SCHAR_MAXsigned char的最大值
SCHAR_MINsigned char的最小值
UCHAR_MAXunsigned char的最大值
SHRT_MAXshort的最大值
SHRT_MINshort的最小值
USHRT_MAXunsigned short的最大值
INT_MAXint的最大值
INT_MINint的最小值
UNIT_MAXunsigned int的最大值
LONG_MAXlong的最大值
LONG_MINlong的最小值
ULONG_MAXunsigned long的最大值
LLONG_MAXlong long的最大值
LLONG_MINlong long的最小值
ULLONG_MAXunsigned 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类型
Uunsigned int
Llong
UL/LUunsigned long
LLlong long
ULL/LLUunsigned long long
  • 不区分大小写。
  • 若类型不能容纳该值,则:int -> long -> long longunsigned 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::coutchar类型智能处理为字符。

char字面量

char a = 'A';

该表示法优于数值编码,不需知道编码方式。

转义序列的编码

字符名称ASCII符号C++代码十进制ASCII码八进制ASCII码十六进制ASCII码
换行符NL (LF)\n100120xA
水平制表符HT\t90110x9
垂直制表符VT\v110130xB
退格BS\b80100x8
回车CR\r130150xD
振铃BEL\a7070x7
反斜杠\\\9201340x5C
问号??630770x3F
单引号\’390470x27
双引号\"340420x22
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 charunsigned char

int不同的是,char在默认情况下既不是无符号,也不是有符号,是否有符号由C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。如果char有某种特定的行为对您来说非常重要,则可以显式地将类型设置为signed charunsigned char

如果将char用作数值类型,则unsigned charsigned 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

cincout将输入和输出看作是char流,因此不适于用来处理wchar_t类型。iostream头文件的最新版本提供了作用相似的工具—wcinwcout,可用于处理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_tchar32_t

随着编程人员日益熟悉Unicode,类型wchar_t显然不再能够满足需求。事实上,在计算机系统上进行字符和字符串编码时,仅使用 Unicode码点并不够。具体地说,进行字符串编码时,如果有特定长度和符号特征的类型,将很有帮助,而类型wchar_t的长度和符号特征随实现而已。因此,C++11新增了类型char16_tchar32_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_tchar32_t也都有底层类型:一种内置的整型,底层类型的选择取决于实现。
wcinwcoutchar16_tchar32_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 log10​223≈7,有效数字为6确保四舍五入后也数值精准。
这3种类型的指数范围至少是−37到37( 2 127 ≈ 1 0 38 2^{127} \approx 10^{38} 2127≈1038 ),可以从头文件cfloat或float.h中找到系统的限制。

浮点字面量后缀

后缀
默认double
f/Ffloat
l/Llong double

算术运算符

运算符
+
-
*
/
%求模

类型转换

以{ }方式初始化时进行的转换

int a=66;
char b=a; //隐式类型转换
char c{a}; //报错
int d=66666666666666666; //-385439062
int e = {66666666666666666}; //报错

以{ }方式初始化时,其对类型转换的要求更严格,列表初始化不允许缩窄,即不允许将变量无法表示的值赋值给该变量。

C++11版本校验表

  1. 如果有一个操作数的类型是long double,则将另一个操作数转换为long double。
  2. 否则,如果有一个操作数的类型是double,则将另一个操作数转换为double。
  3. 否则,如果有一个操作数的类型是float,则将另一个操作数转换为float。
  4. 否则,说明操作数都是整型,因此执行整型提升
  5. 在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
  6. 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型。
  7. 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
  8. 否则,将两个操作数都转换为有符号类型的无符号版本。

整型提升

在计算表达式时,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_tchar16_tchar32_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与其他字符串前缀结合使用(LRuRUR),以标识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打印的是指针所指向的内存地址。

上述代码可以说明ptr1array1等价吗?不可以,其只能说明数组名array1存储了数组的第一个元素的地址,其是一个指针,且指向的是int类型数据对象。

array1ptr1一样,都可使用数组表示法与指针表示法:

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

探究二

ptr1array1究竟是否等价呢?答案是不等价:

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;

ptr1array1又是否等价呢?答案是不等价:

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 相差一个指针类型字节

ptr1array1一样,都指向数组第一个元素的地址,指向的都是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

ptr1ptr2本身都是指针,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::stringstd::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;

newdelete使用规则

  • 不要使用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类确实使用newdelete来管理内存,但这种工作是自动完成的。

vectorarray对象不禁止下标越界的行为,您仍可编写不安全的代码,然而,您还有其他选择。一种选择是使用成员函数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循环。这简化了一种常见的循环任务:对数组(或容器类,如vectorarray)的每个元素执行相同的操作。
  • 要修改数组的元素,需使用带&的循环变量语法。符号&表明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++标准提供了另一种表示方式:andornotandornot都是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;
}

枚举量作switchcase标签,枚举量会被提升为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 elseswitchcontinue等)来控制程序的流程。

函数

基本知识

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++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。

  • rodentsrats的引用,它们指向相同的值和内存单元。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_throwsdup = 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与右值匹配。注意到与r1r3匹配的参数都与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;
}

函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如intdouble)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类(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)。

重载解析步骤:

  1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  2. 匹配特征标,可隐式类型转换。
  3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

何为最佳:

  1. 完全匹配,但常规函数优先于模板。
  2. 提升转换(例如,charshorts自动转换为intfloat自动转换为double)。
  3. 标准转换(例如,int转换为charlong转换为double)。
  4. 用户定义的转换,如类声明中定义的转换。

完全匹配允许的无关紧要转换:

从实参到形参
TT&
T&T
T[]*T
T(argument-list)T(*)(argument-list)
Tconst T
Tvolatile 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;
    //...
}

decltypetypedef组合使用:

//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;
}

无法预先知道将xy相加得到的类型,且由于参数xy未声明,无法使用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*,以便能够赋给任何指针类型。
  • p2buffer的地址,但类型不同。
  • 定位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

相关文章

  • mybatis plus多表查询的扩展
    mybatisplus提供了简单的CURD操作,但是有时我们的业务需要要求进行多表查询,这个时候,我们就需要加入多表查询的扩展了。 mybatis-plus-join,基于mybatis-plus的所有优点,然后还支持连表查询,还支持一对多,一对一的查询。mybatis-plus-join是mybatisplus的一个多表插件,上手简单,几分钟就......
  • cpp primer plus 第七章
    7.1函数基本知识7.1.1定义函数函数分为两类:有返回值与无返回值的函数。对于有返回值的函数,必须使用返回语句,将值返回给调用函数。若函数包含多条返回语句,则函数在执行第一条返回语句后结束。7.1.2函数原型声明函数如果在main函数后方,则在前面声明函数(复制函数定义中的......
  • SpringBoot:Web开发(基于SpringBoot使用MyBatis-Plus+JSP开发)
    目录前期准备构建项目(IDEA2023.1.2,JDK21,SpringBoot3.3.3)添加启动器Model准备这里我们利用MybatisX插件生成我们所需要的实体类、数据访问层以及服务层注意选择MyBatis-Plus3以及Lombok然后再在service接口中定义我们所需要的方法以及实现类(利用MyBatis-Plus省去我们......
  • 尤雨溪推荐的拖拽插件,支持Vue2/Vue3 VueDraggablePlus
    大家好,我是「前端实验室」爱分享的了不起~今天在网上看到尤雨溪推荐的这款拖拽组件,试了一下非常不错,这里推荐给大家。说到拖拽工具库,非大名鼎鼎的的Sortablejs莫属。它是前端领域比较知名的,且功能强大的工具。但我们直接使用Sortablejs的情况很少,一般都是使用基于它的......
  • 标准的vue3 elementplus格式,不用export default
    <template><div><!--查询表单--><el-form:inline="true":model="filters"class="demo-form-inline"><el-form-itemlabel="产品料号"><el-inputv-model="filters.......
  • Mybatis与Mybatis-plus的比较
    MyBatis和MyBatis-Plus都是流行的JavaORM框架,它们在处理数据库操作时各有优势和特点。以下是对两者的比较:MyBatisMyBatis是一个成熟的ORM框架,它提供了映射SQL语句到Java对象的能力。以下是MyBatis的一些优缺点:优点:灵活性高:MyBatis允许开发者编写原生SQL,提......
  • MyBatis-Plus动态表名
    MyBatis-Plus动态表名一、早期方案1.1MyBatis-Plus版本1、添加MyBatis-Plus依赖<dependency>   <groupId>com.baomidou</groupId>   <artifactId>mybatis-plus-boot-starter</artifactId>   <version>3.5.1</version></dependency>......
  • 【Stable Diffusion】最新换脸模型:IP-Adapter Face ID Plus V2 WebUI 效果超赞!(附模型
    ControlNet是StableDiffusionWebUI中功能最强大的插件。基于ControlNet的各种控制类型让StableDiffusion成为AI绘图工具中最可控的一种。IPAdapter就是其中的一种非常有用的控制类型。它不仅能够实现像Midjourney一样的“垫图”功能,还能用来给肖像人物换脸......
  • 锋哥写一套前后端分离Python权限系统 基于Django5+DRF+Vue3.2+Element Plus+Jwt 视频
    大家好,我是java1234_小锋老师,最近写了一套【前后端分离Python权限系统基于Django5+DRF+Vue3.2+ElementPlus+Jwt】视频教程,持续更新中,计划月底更新完,感谢支持。视频在线地址:打造前后端分离Python权限系统基于Django5+DRF+Vue3.2+ElementPlus+Jwt视频教程(火爆连载更新中......
  • c++primer第四章复合类型学习笔记
    数组数组创建声明:存储元素类型数组名数组的元素个数#include<iostream>usingnamespacestd;intmain(){intyams[3];yams[0]=7;yams[1]=8;yams[2]=6;intyamcosts[3]={20,30,5};cout<<"Totalyams=";cout<<......