1. 数据类型
1.1 整型
整型用于表示整数,C++ 中的整型分为有符号整型和无符号整型两种,如下所示:
// 有符号整型
int a = -5;
cout << "有符号整型值: " << a << endl;
// 无符号整型
unsigned int b = 5;
cout << "无符号整型值: " << b << endl;
// 整型表示范围
cout << "有符号整型范围:从 " << INT_MIN << " 到 " << INT_MAX << endl;
cout << "无符号整型范围:从 0 到 " << UINT_MAX << endl;
有符号整型包含正数、负数和零,而无符号整型只包含非负数。因此需要使用负数的情况下选择有符号整型,不需要负数的情况下选择无符号整型。
1.2 浮点型
浮点型用于表示小数,C++ 中提供了两种浮点型数据类型,分别是单精度浮点型和双精度浮点型。单精度浮点型有7位有效数字,双精度浮点型有15位有效数字。若浮点型字面值以科学计数法表示,指数部分以 E 或 e 标识。
float a = 1.1; // 单精度
double b = 2.2; // 双精度
double c = 6.022e5; // 科学计数法表示
1.3 字符型
字符型用于显示单个字符,如下所示:
char a = 'a'; // 以字符的形式
char b = 98; // 以ASCII码的形式
注意: 字符型变量并不是把字符本身放到内存中存储,而是将对应的 ASCII 码值放入到内存中,因此在定义时也可以使用 ASCII 码值来给字符变量赋值。
1.4 字符串
字符串用于储存一串字符,字符串可以包含任意数量的字符、数字、标点符号等,如下所示:
const char* str = "Hello, World!";
注意: 编译器在每个字符串的结尾处自动添加一个空字符。
1.5 布尔类型
布尔类型用于表示真假,bool 类型只有两个值:true 表示真,输出为1;false 表示假,输出为0。
bool flag = false;
cout << flag << endl;
if ( 3 < 4 )
flag = true;
cout << flag << endl;
1.6 转义字符
用于表示一些不能显示出来的 ASCII 字符,常用的转义字符如下:
转义字符 | 含义 | 转义字符 | 含义 | 转义字符 | 含义 |
---|---|---|---|---|---|
\n | 换行符 | \t | 横向制表符 | \r | 回车符 |
\ " | 双引号 | \ ’ | 单引号 | \ \ | 反斜线 |
\b | 退格符 | \f | 换页符 |
2. 类型处理
2.1 指定字面值的类型
通过添加前缀或者后缀,可以改变整型、浮点型的默认类型,如下所示:
42u // 无符号型
42.3f // float型
2.2 sizeof函数
sizeof 函数用于计算一个对象或类型所占用的字节数,用法如下所示:
int a = 10;
cout << sizeof(a) << endl;
2.3 隐式类型转换
在C++中,隐式类型转换指的是编译器自动将一个数据类型转换为另一个数据类型的过程,而无需显式地进行转换。
隐式类型转换通常发生在以下几种情况下:
算术运算: 在算术运算中,如果操作数的类型不相同,编译器会将它们转换为同一类型
赋值操作: 在赋值操作中,如果赋值号右侧的表达式类型与左侧变量的类型不匹配,编译器会进行隐式转换。
函数调用: 在调用函数时,如果传递的参数类型与函数参数的类型不匹配,编译器会尝试进行隐式转换。
int a = 10;
double b = 3.5;
double result = a + b; // 'a' 被隐式转换为 double
int x = 7.8; // '7.8' 被隐式转换为 '7'
2.4 进制
整型字面值默认写作十进制数,但是可以将其转换成八进制数或十六进制数;以0开头的整数代表八进制数,以0x 或 0X开头的代表十六进制数。例如可以用下面任意一种形式来表示数值 20:
20 // 十进制
024 // 八进制
0x14 // 十六进制
3. 变量
变量提供一个具名的、可供程序操作的储存空间。
3.1 变量定义
变量定义的基本形式如下所示:
int a, b, c;
1.初始化
初始化是指变量在创建时就获得了一个值,初始化有好几种方式,如下所示:
int a = 0;
int a = {0};
int a{0};
int a(0);
注意: 初始化不是赋值,初始化是创建变量时赋予其初始值,赋值是把对象当前的值擦除然后用一个新值来替代。
2.默认初始化
如果定义变量时没有指定初始值,则变量被默认初始化,由变量类型决定默认值是什么。
// 未指定初始值,默认初始化
int a; // 一般为 0
注意: 定义于函数体之外的变量被默认初始化为 0,定义于函数体内的内置类型变量不被默认初始化,如果试图拷贝或以其它形式访问将引发错误。
3.2 变量声明
声明规定了变量的类型和名字,使得变量名字为程序所知,如果只想声明一个变量,可以在变量名前添加关键字extern。extern 关键字在 C++ 中用于声明一个变量或函数,而不进行定义。这意味着你告诉编译器这个变量或函数在其他地方定义,并且在当前文件中只做声明,使得编译器知道它的存在,但不会为其分配内存或生成相应的代码。
extern int globalVariable1; // 在另一个文件中定义了全局变量
int main() {
//extern int globalVariable2 = 42; // 在函数体内部初始化extern标记的变量会导致错误
globalVariable1 = 42; // 可以直接使用extern声明的变量
return 0;
}
注意: extern 关键字用于声明一个变量,而不是定义它。在函数体内部试图初始化由 extern 关键字标记的变量时将引发错误,因为它实际上是在声明的同时尝试定义变量,而函数体内的变量初始化通常会被视为定义。
3.3 变量命名规则
变量名由字母、数字和下划线组成,其中必须以字母或下划线开头,且对大小写敏感。具体规则如下:
1.标识符要体现实际含义;
2.变量名一般用小写字母;
3.用户自定义的类名一般大写字母开头;
4.标识符由多个单词组成,中间应有明显区分,比如中间添加下划线区分。
3.4 变量名的作用域
作用域是程序的一部分,C++ 中大多数作用域都以花括号分隔。作用域能彼此包含,被包含的作用域被称为内层作用域,包含着别的作用域的作用域被称为外层作用域。作用域中一旦声明某个名字,它所嵌套的所有作用域都能访问该名字。
#include <iostream>
int globalVar = 10;
void outerFunction() {
int outerVar = 20;
void innerFunction() {
// 在内部函数中访问外部函数的变量
std::cout << "innerFunction: outerVar = " << outerVar << std::endl;
}
// 调用内部函数
innerFunction();
}
int main() {
// 在主函数中访问全局变量和调用外部函数
std::cout << "globalVar = " << globalVar << std::endl;
outerFunction();
return 0;
}
注意: 如果外层作用域用到某变量,则不宜在内层作用域定义一个同名的变量。
4. 复合类型
4.1 引用
引用为对象起了另外一个名字,定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。引用绑定后不能重新绑定,因此引用必须初始化。
int ival = 1024;
int & reval = ival; // reval是ival的另一个名字;
注意: 引用并非对象,它只是为一个已经存在的对象起一个别名,因此引用本身不是一个对象,不能定义引用的引用。
4.2 指针
指针是指向另外一种类型的复合类型。指针本身是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。指针无需在定义时赋初值,但是在块作用域内定义的指针如果没有被初始化将拥有一个不确定的值,如果试图拷贝或以其它形式访问将引发错误。
1.获取对象的地址
指针存放某个对象的地址,使用取地址符可以获得该地址。
int a = 10;
int *p = &a;
注意: 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
2.利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符来访问对象。对指针解引用会得到指针指向的对象,如果给解引用的结果赋值,也就是给指针所指的对象赋值。
int a = 10;
int *p = &a;
cout << *p << endl;
*p = 20;
cout << a << endl;
cout << *p << endl;
3.空指针
空指针不指向任何对象,得到空指针最直接的方法就是用字面值 nullptr 来初始化指针。
int * p = nullptr; // 等价于int *p = 0;
int a = 10;
p = &a; // 可以给空指针赋一个新值
4.赋值和指针
指针和引用都能提供对其它对象的间接访问,但二者有很大区别。其中最重要的一点就是引用本身并非一个对象,一旦定义了引用就不能再绑定到其它对象,但指针没有这种限制,给指针赋值就是令它存放一个新的地址。
int i = 42, val = 32; // 定义两个int型变量
int *p = &i; // 指针p指向i
p = &val; // 指针p改为指向val
5.其他指针操作
只要指针有一个合法的值,就能将它应用到条件表达式中。如果指针的值为 0,条件表达式结果取 false,否则为 true。
int val = 42;
int *p1 = 0; // p合法,是一个空指针
int *p2 = &val;
if(p1) // p表示false
//.....
if(p2) // p2表示true
//....
注意: 对于两个类型相同的指针,可以用相等操作符或者不等操作符来比较它们,比较的结果是布尔值。
4.3 复合类型的声明
1.定义多个变量
经常有一种错误观点,在定义语句中类型修饰符作用于本次定义的全部变量。
int *p1, p2; // p1是指针,p2是int;
2.指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当多个修饰符连写在一起时,按照其逻辑关系解释即可。
int ival = 1024;
int * p1 = &ival; // p1指向一个int型的整数ival
int **p2 = &p1; // p2指向一个int型的指针p1
3.指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针,但可以对指针进行引用。
int i = 42;
int* p = &i;
int*& r = p; // r是一个对指针p的引用
cout << *r << endl;
注意: 面对一条比较复杂的指针或引用的声明语句时,从右往左阅读有助于弄清楚他的真实含义。
5. const限定符
5.1 const与常量
有时候我们希望定义这样一种变量,它的值不能被改变。为了满足这样的要求,可以用关键字const对变量的类型加以修饰。
int i = 42;
const int j = i;
int k = j;
注意: 因为 const 对象一旦创建其值就不能再改变,所以 const 对象必须初始化。可以利用一个 const 对象去初始化一个非 const 对象,反过来也可以。
1.const对象仅在文件内有效
编译时编译器将在编译过程中用到 const 变量的地方都替换成对应的值。默认情况下 const 对象仅在文件内有效,当多个文件中出现了同名的 const 变量时,其实是在不同文件中分别定义了该变量。如果想在多个文件之间共享 const 对象,需要在变量的定义前添加extern。
5.2 const与引用
可以把引用绑定到 const 对象上,称之为对常量的引用,但对常量的引用不能用来修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci;
r1 = 42; // 错误,r1是对常量的引用
int &r2 = ci; // 错误,试图让一个普通引用指向一个常量对象
1.初始化和对const的引用
引用的类型必须与所引用的对象的类型一致,初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。对常量的引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因此引用的对象可能是一个非常量,所以可以通过其他途径改变它的值。
int i = 42;
const int &r1 = i; // 正确
const int &r2 = 42; // 正确,r2是一个常量引用
const int &r3 = r1*2; // 正确,r3是一个常量引用
int &r4 = r1*2; // 错误,r4是一个普通引用
int &r5 = i;
const int &r6 = i; // 可以通过r5修改i的值,但r6不能修改i的值
r5 = 0; // 正确
//r6 = 0; // 错误
5.3 const与指针
要想存放常量对象的地址,只能使用指向常量的指针,指针的类型必须与所指对象的类型一致,而且指向常量的指针不能用于改变其所指对象的值。允许一个指向常量的指针指向一个非常量对象,但不能通过该指针来改变对象的值,不过可以通过其它途径更改对象的值,如下所示:
const double pi = 3.14; // pi是个常量,它的值不能改变
double *ptr = π // 错误,ptr是一个普通指针
const double *cptr = π // 正确,指向常量的指针,指针的指向可以修改
*cptr = 42; // 错误,不能给*cptr赋值来修改pi的值
double dval = 4.2;
cptr = &dval; // 正确,指向常量的指针指向一个非常量对象
*cptr = 43; // 错误,不能给*cptr赋值来修改dval的值
double& i = dval; // 可以通过改变i的值来改变dval
i = 44;
cout << *cptr << endl;
注意: 指向常量的指针在普通指针前加 const 进行限定,允许对指针指向进行修改,但不能通过该指针来修改指针指向的那个变量的值。
5.4 顶层const和底层const
指针本身是一个对象,其又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const表示指针本身是个常量,用名词底层const表示指针所指的对象是一个常量。更一般的是顶层 const 可以表示任意的对象是个常量。下面的代码帮助理解:
#include<iostream>
using namespace std;
int main()
{
int i = 42;
int j = 6;
const int * p1 = &i; // 底层const,可以修改指针指向
int * const p2 = &j; // 顶层const,不可以修改指针指向
p1 = &j;
cout << *p1 << endl;
*p2 = 42;
cout << *p2 << endl;
return 0;
}
注意: 简单来说顶层 const 是一个常量,指向的变量不可以修改,但可以修改指向的那个变量的数值。底层 const 表示指向的对象是一个常量,因此可以修改指针的指向,但不能通过该指针来修改指针指向的那个变量的值。
5.5 constexpr
1.constexpr变量
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。C++允许把变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,该变量必须用常量表达式初始化。
constexpr int mf = 20; // 20是常量表达式
2.指针和constexpr
在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关。
int x = 42;
// 指针q的指向不能修改,但可以修改储存的值
constexpr int *q = &x;
*q = 43;
cout << *q << endl;
6. 类型说明符
6.1 类型别名
类型别名是某种类型的代名词。使用类型别名可以让复杂的类型名字变得简单明了,有两种方法可用于定义类型别名,如下所示:
// 1.使用关键字typedef
typedef double wages; // wages是double的同义词
// 2.使用using声明
using SI = Sales_item; // SI是Sales_item的同义词
6.2 auto类型说明符
编程时常常需要把表达式的值赋给变量,这就要求在声明变量时知道表达式的类型。C++ 引入了auto类型说明符,用它就能让编译器替我们去分析表达式的类型。
auto i = 0, *p = &i;
注意: auto 让编译器通过初始值来推算变量的类型,因此 auto 定义的变量必须有初始值。
1.复合类型、常量、auto
编译器推断出来的auto类型有时候和初始值的类型不完全一样,编译器会适当的改变结果使其更符合初始化规则。
int i = 0;
const int ci = i,&cr = ci;
auto b = ci; // b是一个整数,ci的顶层const特性被忽略
auto c = cr; // c是一个整数
6.3 decltype类型说明符
有时候会遇到这种情况,希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。C++ 引用了decltype,它的作用是返回括号内的变量的数据类型或函数返回的数据类型。
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
decltype 处理顶层 const 和引用的方式与 auto 有些许不同,如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型时包括顶层 const 和引用在内,如下所示:
const int ci = 0, & cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&, y绑定到变量x
1.decltype和引用
如果表达式不是一个变量,decltype 会根据表达式的类型来推断结果的类型。
int i = 42;
decltype((i))d; // 错误, d 是 int&, 必须初始化
decltype(i) e; // 正确, e是一个未初始化的int
// (i)是一个表达式,decltype将i视为一个被括号括起来的表达式,而不是一个单一的变量。
// (i)是引用,decltype((i))的结果也是引用
// decltype((variable))的结果永远是引用,
// decltype(variable)结果只有当variable本身就是一个引用,结果才是引用
7. 自定义数据类型
C++ 语言允许用户以类的形式自定义数据类型,库类型string、istream、ostream等也都是以类的形式定义的。
7.1 命名空间的using声明
目前为止用到的库函数基本都存在于命名空间std中,例如:
std::cin;
std::cout;
std::endl;
域操作符的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。使用using声明之后,程序中就无需专门的前缀也能使用所需的名字了。
using std::cin;
using std::cout;
using namespace std;
注意: 位于头文件的代码一般来说不应该使用 using 声明,因为头文件的内容会拷贝到所有引用的文件中去,可能会引发始料未及的冲突。
7.2 编写自己的头文件
类一般都不定义在函数体内,当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。为了确保各个文件中类的定义一致,类通常被定义在头文件中。确保头文件多次包含仍能安全工作的常用技术是预处理器,它由 C++ 语言从 C 语言继承而来。C++ 程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include<string>
struct Sales_data{
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
// #define指令把一个名字设定为预处理变量
// #ifdef当且仅当变量已定义时为真
// #ifndef当且仅当变量未定义时为真
// 一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。