C++ Primer 学习笔记 第二章 变量和基本类型
C++是一种静态数据类型语言,它的类型检查发生在编译时。
基本内置类型:C++定义了一套包括算数类型和空类型在内的基本数据类型。
算数类型:整型(字符、整型数、布尔值)和浮点数。
空类型:不对应具体的值,仅用于特殊场合,常见的有函数不返回任何值时用空类型作返回类型。
算数类型的尺寸在不同机器上有所差别,C++标准规定了尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。
一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值,也就是说,一个char的大小和一个机器字节一样。
一个int至少要和一个short一样大,一个long至少要和一个int一样大,一个long long至少要和一个long一样大。long long是C++11标准中新定义的。
实际应用中,long一般和int一样有同样的尺寸,如果要表示的数大小超过int范围,选用long long。
char类型在一些机器上是有符号的,一些机器上是无符号的,要避免含char类型的算术表达式,有时会出错。如果要使用一个不大的整数,明确它是signed char或unsigned char。
浮点数运算选double,float通常精度不够,而float和double的计算代价相差无几,对某些机器,double比float还快。
非布尔类型算数值赋给布尔类型时,值为0结果为false,否则为true。布尔类型赋给非布尔类型时,值为false结果为0,值为true结果为1。
赋给无符号类型的数超过它的表示范围时,结果是初始值对无符号类型表示的数值总数取模后的余数,即截取要溢出的数该无符号类型的位数,再将值赋给该无符号类型对象,如unsigned char有8位,最大值为255,如将257赋值给它,257的二进制表示为100000001,截取后8位即为1;也可将257对该类型所能表示的数值总数256取模,结果还是1。
赋给带符号类型一个超出它表示范围的值时,结果是未定义的(与编译器有关)。
当一个算术表达式中有无符号数又有有符号数时,有符号数会转化为无符号数。负数转化为无符号数即该无符号数类型能表示的最大数与负数的和,若有符号数为-42,int为4字节,转化为无符号数即2的32次方-42=4294967254。
两个无符号数相减时,结果必为正数。如42-10=32;10-42=4294967264(将-32的补码当做正数的二进制码所表示的数,即2的32次方-32):
int main() {
unsigned a = 10, b = 42;
cout << a - b << endl; // 输出4294967264
}
不要混用带符号类型和不带符号类型,如a=-1,b=1,若a为int,b为unsigned int,两者混用时,a的值变为4294967295(a转化为无符号数即2的32次方-1=4294967295)。
整型字面值可以写作十进制数(20)、八进制数(以0开头:024)、十六进制数(以0x或0X开头:0x14)。默认情况下,十进制字面值是带符号数,八进制和十进制字面值既有可能是带符号的也有可能是不带符号的。十进制字面值类型是int、long、long long中最小能容纳的一个;八进制和十六进制字面值类型是能容纳其数值的int、unsigned int、long、unsigned long、long long、和unsigned long long中尺寸最小的,如果一个字面值连与之关联的最大数据类型都放不下会产生错误。short没有对应的字面值。整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数,如-42,负号不在字面值内,它的作用是对字面值取负。
浮点数字面值默认是double,表现形式是小数或科学计数法,科学计数法以e或E后面的数字表示指数部分,如3.14可以写为0.314e1(0.314*10的一次方)。当浮点数的整数部分为0时0可以省略,如.3代表0.3,也可以和科学计数法结合,如3=.3e1。当浮点数的小数部分为0时可以省略,如1.0=1.,字面值1.默认为double型,字面值1默认为int型。
由单引号括起来的一个字符是char型字面值,双引号括起来的零个或多个字符构成字符串型字面值。字符串字面值的类型实际上是由常量字符构成的数组(即const char *类型),编译器会在每个字符串的结尾处添加一个空字符’\0’表结尾,因此字符串字面值的长度要比它的内容多1。如:'a’表示单独的字符a,“a"代表一个常量字符的数组,该数组包含字符a和空字符。如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔时,他们实际是一个整体,如"aaa” “ddd"等价于"aaaddd”,因此当要书写的字符串过长,可以将它分成两行写。
转义序列:程序员不能直接使用的两类字符一是不可打印的字符,如退格,二是在C++中有特殊含义的字符,如单引号、双引号等。要使用这些时,需要用到转义序列,转义序列均以反斜线开始,C++规定的部分转义序列有:
名称 | 转义序列 |
---|---|
换行符 | \n |
横向制表符 | \t |
报警(响铃)符 | \a |
纵向制表符 | \v |
退格符 | \b |
双引号 | \" |
单引号 | \’ |
反斜线 | \\ |
回车 | \r |
程序中,转义序列被当做一个字符使用。
也可以使用泛化的转义序列,形式是\x后紧跟一个或多个十六进制数字或\后紧跟1个、两个或三个八进制数字,数字部分是字符对应的数值。如使用的Latin-1字符集,则\12表换行、\115表字符M。如\后跟着三个以上八进制数,则只有前三个数构成转义序列,但\x后的所有数字都被看做转义序列的一部分。
指定字面值的类型:
字符和字符串字面值:
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode 16 字符 | char16_t |
U | Unicode 32 字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面常量) | char |
整型字面值:
后缀 | 最小匹配类型 |
---|---|
u或U | unsigned |
l或L | long |
ll或LL | long long |
使用l或L时用大写,与1区分度比较高。
浮点型字面值:
后缀 | 类型 |
---|---|
f或F | float |
l或L | long double |
我们可以将U和L(LL)结合使用,效果是根据字面值大小从unsigned long 和 unsigned long long 中选择一个。
true和false是布尔类型字面值。nullptr是C++11中新引入的指针字面值。
变量提供一个具名的、可供程序操作的存储空间。定义变量时首先是类型说明符,之后紧跟一个或多个变量名组成的列表,变量名以逗号分隔。
对象创建时获得了一个特定的值就说这个对象被初始化了,用于初始化变量的值可以是任意复杂的表达式(函数返回值也可以),在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。如:
int a = 1, b = 3 * a; //a初始化为1,b初始化为3
初始化与赋值不同,初始化是创建变量时赋予其一个初值,赋值是把对象当前值擦除,再用一个新值替代。
C++11新标准下,初始化可以用花括号(列表初始化),如int型对象a初始化为0:
int a = 0;
int a = {0};
int a{0};
int a(0);
花括号初始化时,如果初始值存在丢失信息的风险,则编译器报错:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; //错误,转换未执行,因为存在丢失信息的危险,C++11新标准的方法
int c(ld), d = (ld); //正确,转化执行,且确实丢失了部分值,这种方法是用了构造函数
但在实际运行中,有些编译器花括号初始化也会转换,这是编译器的扩展,在其他编译器上不一定能通过。
如果定义变量时没有指定初值,则变量会被默认初始化,默认值由变量类型决定,同时定义变量的位置也会对此有影响。内置类型的变量未被显式初始化,它的值由定义的位置决定,定义于函数体之外的变量初始化为0,定义在函数体内的非static变量值是未定义的(与编译器有关,有些是随机值,有些是0,最好全都初始化),如试图拷贝或访问此类值会引发错误。
类类型对象没有显式初始化,其值由类确定,如string类型会非显式地初始化为一个空串。
以下定义:
int a;
int b = a = 1; // a、b值都被定义为了1
C++支持分离式编译,即将程序分割为若干个文件,每个文件都可以被独立编译。为支持分离式编译,C++将变量的声明和定义区分开来,声明使得名字为程序所知,一个文件如果想使用别处定义的名字必须包含对那个名字的声明,定义负责创建与名字关联的实体。如果想声明而非定义,就在变量名前加extern并且不要显式地初始化变量:
extern int i; // 声明i,而非定义i
extern int i = 2; // 定义i,抵消了extern的作用
函数体内部,如果试图初始化一个由extern关键字标记的变量会引发错误。
变量可以被定义一次,但可以多次声明。
C++标识符必须由字母、数字、下划线组成,并且要由字母或下划线开头,标识符长度无限制。由于标准库的命名规则,用户自定义标识符中最好不要出现两个连续下划线,也不能以下划线紧连大写字母开头,定义在函数体外的标识符不要以下划线开头,防止标准库更新造成重名。不能与关键字和操作符替代名(如and、&&两个用在程序中可相互替换)重名。
变量命名的约定俗成规范:(1)标识符要能体现实际含义。
(2)变量名用小写字母。
(3)用户自定义类名一般以大写字母开头。
(4)若标识符由多个单词组成,单词间应该有明显区分,如:studentLoan或studeng_loan。
全局作用域:定义在函数体之外。
块作用域:定义在花括号内。
最好在你第一次使用变量时再定义它。
作用域可以相互包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字,同时,允许在内层作用域中重新定义外层作用域中已有的名字。
在内层作用域想访问外层变量时,用作用域操作符,因为全局作用域本身并没有名字,所以当作用域操作符左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量:
#include <iostream>
using namespace std;
int a = 2;
int main(){
cout << a << endl; // 输出2
int a = 1;
cout << a << endl; // 输出1
cout << ::a << endl; // 输出2
return 0;
}
如果有函数可能用到全局变量,不宜再定义一个同名局部变量。
复合类型指基于其他类型定义的类型。C++中有引用和指针等。
引用为对象起了另一个名字,通过将声明符写成&d的形式定义引用类型,d为声明的变量名。:
int ival = 1024;
int &refVal = ival; // refVal是ival的另一个名字
int &refVal2; // 报错:引用类型必须被初始化
定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用不是一个对象,而是一个已经存在对象的别名,因此不能定义引用的引用(引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起)。
一条语句中可以定义多个引用,每个引用标识符必须以&开头:
int a = 10, &b = a;
b += 1;
cout << a << endl; //输出11
对于两个引用:
int a = 10, &b = a;
double c = 3.14, &d = c;
d = b;
cout << c << endl; // 输出10
指针是指向另外一种类型的复合类型,跟引用类似,也实现了对其他对象的间接访问,但与引用也有许多不同,首先,指针是一个对象,允许对指针赋值和拷贝,在指针的生命周期内它可以先后指向几个不同的对象,其二,指针无需在定义时赋值。与其它内置类型相同,在块定义域内定义的指针未初始化也会拥有一个不确定的值。
指针定义的方法将声明符写为*d的形式,d是变量名,如果一条语句中定义了几个指针变量,每个变量前都要加星号,且指针存放的是某个对象的地址值,要想获得该地址,需要使用取地址符&:
int a, *b, *c = &a; // c中存放a的地址,a必须写在c之前
int *d = c; // d也是指向a的指针
引用不是对象,没有实际地址,不能定义指向引用的指针。(但我测试可以):
int main() {
int i = 0, &refi = i;
int* p = &refi;
cout << *p << endl;
}
指针值应属于以下四个状态之一:(1)指向一个对象。
(2)指向相邻对象所占空间的下一个位置。
(3)空指针。
(4)无效指针,上述情况外的其他值。
试图拷贝或以其它方式访问无效指针的值将引发错误,编译器不负责检查此类错误。
访问以上(2)、(3)类型的指针(假定的)对象的行为不被允许,这样做了后果无法估计。
利用指针访问对象,需要使用解引用符*:
int a = 1, *b = &a;
cout << *b; // 输出a
*b = 3; // a的值变为3
解引用操作仅适用于那些确实指向了某个对象的指针。
int i = 0, *p = &i;
int &a = *p; // a是i的引用,*p解引用后代表对象i
a = 4;
cout << i << endl; //输出4
空指针不指向任何对象,以下为生成空指针方法:
int *p1 = nullptr; // 等价于int *p1 = 0,C++11新标准
int *p2 = 0;
int *p3 = NULL; // 等价于int *p3 = 0
字面值nullptr是一种特殊类型的字面值,它可以被转化为任意其它的指针类型。过去的程序还会用到名为NULL的预处理变量给指针赋值,这个变量在头文件cstdlib中定义,值为0。
预处理器是运行于编译过程之前的一段程序,预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而不用在前面加std::。当用到一个预处理变量时,预处理器会自动将它替换为实际值,因此用NULL和0初始化指针是一样的。新标准下最好用nullptr,避免使用NULL。
把int变量直接赋值给指针是错误的,即使int值恰好为0。
建议初始化所有指针,尽量等定义了对象后再定义指向它的指针,或初始化为nullptr或0。
指针和引用都能提供对其他对象的间接访问。
如果指针为空指针(值为0),用在条件表达式中就是false,任何非0指针对应的条件值都是true。
对于两个类型相同的合法指针,可以用==或!=来比较它们,如果两个指针存放的地址值相同,则它们相等。
void*是一种特殊的指针类型,可以存放任意对象的地址。一个void*指针存放着一个地址,但我们不知道这个地址中的对象类型。void*指针可以用来和别的指针比较、作为函数输入输出、赋给另外一个void*指针。但不能直接操作void*指针所指对象,因为不知道对象类型,也就无法确定能在这个对象上做的操作。
有一种观点会误以为,在定义语句中,类型修饰符*或&作用于本次定义的全部变量,造成这种错误看法原因之一为:
int* p; // 合法但易产生误导
int* p1, p2; // 定义了指针p1和int型变量p2
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址存放到另一个指针中。通过*的个数区分指针的级别,**表示指向指针的指针:
int a = 0, *p1 = &a, **p2 = &p1; // p2指向p1,p1指向a
**p2 = 1; // 两个解引用符,效果使a的值变为1
引用不是一个对象,因此不能定义指向引用的指针,但指针是对象,所以存在对指针的引用:
int a = 1, *p, *&r = p;
r = &a;
*r = 0;
要想搞清楚r的类型,从右往左阅读r的定义,首先离变量名最近符号(&)对变量类型有最直接影响,因此r是一个引用。声明符其余部分确定r引用的类型是什么,上例中int *表示r引用的是int型指针。
const修饰符修饰的变量值不会改变,尝试改变该值的语句会报错,因此必须初始化:
const int a = getNum(); // 运行时初始化
const int b = 1; // 编译时初始化
默认状态下,const对象仅在文件内有效。以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方替换为相应值,为执行上述替换,编译器必须知道变量初始值,若程序包含多个文件,每个用了const对象的文件都必须能访问到它的初始值,就要在每个文件中都对它有定义,但为避免对同一变量的重复定义,const对象默认情况下仅在文件内有效,当多个文件中出现了同名const变量,其实等同于在不同文件中分别定义了独立的变量。有时候const变量的初始值不是常量表达式,又想在文件间共享,我们这时不需要编译器为每个文件生成独立的变量,即在一个文件中定义const,在其他多个文件中声明并使用它,解决方法是不管是声明还是定义都添加extern关键字:
extern const int a = fuc(); // 定义要加extern关键字
对常量的引用:把引用绑定在const对象上,同样此时不能修改引用所绑定的对象:
const int a = 1;
const int &r1 = a; // 正确
int &r2 = a; // 错误,试图让非常量引用指向常量对象
引用的类型必须与所引用的对象类型一致,但有两个例外,第一个就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
int a = 42;
const int &r1 = a; // 常量引用可以绑定非常量变量
a = 1; // 正确,a不是常量,可以重新赋值,此时r1绑定的对象值也变为了1
r1 = 2; // 错误,r1是常量引用,修改其值会报错
const int &a = 1; // 正确,常量引用可以绑定字面值
const int &b = 1.2; // 正确,常量引用允许以任意表达式的结果赋值,只要该结果能转化为引用的类型即可
const int &c = 2 * a; // 正确,c绑定了值2
int &d = a * 2; // 错误
int a = 1;
const int r1 = a * 2;
a = 2; // r1绑定的值还是2,不会变
当出现以下语句时:
double a = 3.14;
const int &r1 = a;
等价于:
const int temp = a; // 由双精度浮点数生成的一个整型常量
const int &r1 = temp;
此时r1绑定了一个临时量对象,临时量对象是编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名对象。当r1不是常量引用而是普通引用时,就要求引用的类型也是double,因为若r1不是常量,就意味着可以通过r1改变绑定对象的值,而r1绑定的实际上是一个临时变量,要修改的话也是修改的临时变量的值,因此这种行为不合理,C++将这种行为定义为非法。
对const的引用可能引用一个并非const的对象,常量引用仅对引用可以参与的操作作出了限定,因此绑定的对象可以通过其他方式改变值,从而常量引用的值也随之改变。
指针也可以指向常量,指向常量的指针不能用于改变其所指对象的值,想存放常量对象的地址,必须使用常量指针:
const int a = 1;
const int *ptr1 = &a; // 正确
int *ptr2 = &a; // 错误
指针类型必须与其所指对象类型一致,但有两个特例,一是允许令一个常量指针指向一个非常量对象:
int a = 1;
const int *ptr = &a;
与常量引用类似,不能通过指针ptr修改a的值,但可以通过其他方式修改a的值。
指针是对象而引用不是,因此就像其他对象一样,允许把指针本身定义为常量,常量指针必须初始化(常量都要初始化),一旦初始化完成,指针中的地址值就不能改变了,声明方法:
int a = 0;
int *const ptr = &a; // 常量指针指向a
const int b = 1;
const int *const ptr1 = &b; // 指向常量的常量指针
顶层const:表示指针是常量;底层const:表示指针指向的对象是常量。指针类型既可以是顶层const也可以是底层const。
const int a = 1; // 顶层const
执行拷贝操作不会改变被拷贝对象的值,因此拷入拷出的对象是否是常量都没什么影响。但底层const的拷入拷出对象必须具有相同的底层const资格,一般来说,非常量可以拷给常量,反之不行。
常量表达式指值不会改变,并且在编译过程中就能得到结果的表达式。字面值属于常量表达式,用常量初始化的const对象也是常量表达式:
const int a = 2; // a是常量表达式
const int b = a + 1; // b是常量表达式
int c = 1; // c不是常量表达式
const int d = size(); // d不是常量表达式,其值在运行过程中才能得知
C++11新标准规定允许将变量声明为constexpr类型,以便由编译器来验证变量是否是常量表达式,声明为constexpr的变量一定是一个常量,必须用常量表达式初始化:
constexpr int a = 1;
constexpr int b = a + 1;
constexpr int c = size(); // 当size函数是constexpr函数时才是正确声明,否则编译时会报错
constexpr函数足够简单使得编译时就能计算其结果。
如果你认定一个变量是常量表达式,那就把它声明为constexpr类型。
constexpr声明时用到的类型比较简单,值也显而易见,把它们称为字面值类型,算术类型、引用和指针等都属于字面值类型,而自定义的类、IO库、string类型不属于字面值类型,也就不能定义为constexpr。引用和指针定义为constexpr时,初始值必须为nullptr、0或存储于某个固定地址中的对象。函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这类对象,而在函数体之外的对象的固定地址不变,能用来初始化constexpr指针。允许函数定义一类有效范围超出函数本身的变量,这类变量也有固定地址,因此constexpr引用能绑定在这样的变量上,constexpr指针也能指向这样的变量。
constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *a = nullptr; // a是一个指向整型常量的指针
constexpr int *b = nullptr; // b是一个指向整型的常量指针,其可以指向常量也可以指向非常量
constexpr const int *c = &d; // 指向整形常量的常量指针,d必须符合如上所述条件。
类型别名:类型的同义词,定义方法:
传统定义方法:
typedef double wages; // wages是double的同义词
typedef wages base,*p; // base是double的同义词,*p是double*的同义词
C++11新标准方法:
using SI = Sales_item; // SI是Sales_item的别名
要注意:
typedef int *a; // *a是int*的别名
const a b = 0; // b为指向int的常量指针,而非指向常量int的指针
const int *b = 0; // 经常有人把上句中的a替换掉变成此句,这是错误的,此句含义为b为指向常量整型的指针
const a *c; // 与上句不同,c为指针,c指向的对象是int型常量指针,此处可以不初始化,该声明仅代表指向的对象是常量而非c本身是常量
C++新标准引入了auto说明符,用它能让编译器替我们去分析表达式所属的类型,auto定义的变量必须有初始值:
auto item = val1 + val2; // 编译器由val1和val2相加结果推断出item的类型
auto i = 0, *p = &i; // i的类型都是int,p是int型指针
auto sz = 0, pi = 3.14; // 错误,一条声明语句中只能有一个基本数据类型
编译器推断出来的auto类型有时候和初始值的类型并不完全一样:
int i = 0, &r = i;
auto a = r; // a类型为int,而非int &
auto一般会忽略顶层const,保留底层const:
const int ci = i, &cr = ci;
auto b = ci; // b是一个int,ci的顶层const特性被忽略掉
auto c = cr; // c是一个int,cr是ci的别名,ci本身是一个顶层const
auto d = &i; // d类型为const int *(对常量对象取地址是一种底层const)
如希望推断出的auto类型是一个顶层const:
const auto f = ci; // ci的推演类型是int,f类型是const int
还可以将引用的类型设为auto,初始化规则不变:
auto &g = ci; // g是一个整型常量引用,绑定到ci
auto &h = 42; // 错误,不能为非常量引用绑定字面值
const auto &j = 42; // 正确,可以为const引用绑定字面值
用auto声明一个引用时,初始值中的顶层const属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
auto k = ci, &l = i; // k是int型,l是int型引用
auto &m = ci, *p = &ci; // m是常量整型引用,p类型为const int *
auto &n = i, *p2 = &ci; // 错误,n的推断类型为int,p2的推断类型为const int
C++11新标准中引入了新的类型说明符decltype,作用是返回操作数的数据类型:
decltype(f()) sum = x; // sum的类型为函数f的返回类型
以上代码并不实际调用函数f,而仅仅是使用函数f的返回类型作为sum的类型。
如果decltype使用的表达式是一个变量,会返回该变量的类型,包括顶层const和引用:
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型为const int
decltype(cj) y = x; // y的类型为const int&,y绑定到变量x上
decltype(cj) z; // 错误,z类型为const int&,必须初始化引用
引用从来都作为其所指对象的同义词出现,只有用在decltype处时例外。
如果decltype使用的表达式不是一个变量,decltype返回表达式结果对应的类型:
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // b类型为int
decltype(*p) c; // 错误,解引用指针的类型为int&而非int,必须初始化
decltype(i) d; // d为int型
decltype((i)) e; // 错误,e为int&,必须初始化
当decltype的参数是变量名加上一个括号时,编译器会认为这是一个表达式,变量是一种可以作为赋值语句左值的特殊表达式,这样decltype会得到引用类型。
赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型,如果i是int,则表达式i=x的类型是int&:
int a = 3, b = 4;
decltype(a = b) d = a; // d的类型为int&,绑定在a上,a此时值还是3,decltype并不实际计算其参数
d = 777; // a的值也变为了777
cout << a << endl; // 输出777
类定义:以struct开始,后紧跟类名和类体(可为空),类体由花括号包围形成新的作用域,类体右侧的花括号后必须写一个分号,因为类体后面可以紧跟变量名,变量名是在定义该类的对象:
struct Sales_data{
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
} accum, trans, *salesptr;
不建议将对象和类定义在一条语句,以上代码等价于:
struct Sales_data{
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data accum, trans, *salesptr;
C++新标准规定可以为类数据成员提供一个类内初始值,创建对象时该变量会初始化为类内初始值,没有初始值的成员会被默认初始化,即定义的位置在函数体外,默认初始化为0。
string类型有<<、>>、==等操作,<<表示输出字符串,>>表示给字符串赋值,==判断字符串是否相同。
为了各个文件中类的定义保持一致,类通常定义在头文件中,且类所在的头文件名最好和类的名字一样。头文件经常包含只能被定义一次的实体,如类、const对象、constexpr对象。
头文件中如用到string类型,需要包含string头文件,同时main函数中如用到string类型需要再包含一次string头文件,string就被包含了两次。
确保头文件多次包含仍能安全工作的技术是预处理器,是C++由C继承而来。#include就是一项预处理功能,预处理器看到#include时会把指定的头文件内容代替#include。
C++还会用到一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量,预处理变量有两种状态:已定义和未定义。#define是把一个名字设定为预处理变量,#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则后续操作直至遇到#endif为止:
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
当第一次包含Sales_data.h时,#ifndef检查为真,执行后边代码直到endif,预处理变量SALES_DATA_H也变为已定义,后续遇到#ifndef时检查结果就为假。
预处理变量无视C++中作用域规则。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)