「C++」 数据类型
进入 C++
main() 函数
1 |
|
- 函数头:对函数与程序其他部分的接口进行描述
- 函数体:一对花括号之间的部分,描述函数的行为
- 每条完整的指令称为语句,所有的语句都以分号结束
- 返回语句:结束函数
int main()
描述了 main 函数与操作系统之间的接口
- main 函数是程序的唯一入口,一个程序有且只有一个 mian 函数
- main 函数被程序启动代码调用
关于 main 函数的几点说明:
int main(void)
无参数时是否加void
关键字是等价的- 避免使用
main()
和void main()
尽管有时合法 - main 函数最后的
return 0
可选,但此规定不适用于其他函数
注释
1 |
|
预处理器
预处理器指令,指示编译器在实际编译之前所需完成的预处理
所有预处理器指令以 #
开头,预处理指令不是 C++ 语句,因此不以分号 “;” 结尾
例如:#include, #define, #typedef
等
#define
预处理指令#define PI 3.14159
编译前会将代码中的 PI 替换成 3.14159,但不做类型检查,可使用const
变量代替参数宏
#define MIN(a,b) (a < b ? a : b)
,但是有潜在问题,可使用内联函数代替#typedef
创建类型别名#typedef long long ll
#define
创建类型别名存在潜在的问题1
2#define FLOAT_POINTER float *
FLOAT_POINTER pa, pb;定义出的 pa 是
float *
型,pb 却是float
型typedef 可以避免这个问题,并且功能更强大,参见函数指针
头文件名
头文件类型 | 约定 | 示例 | 说明 |
---|---|---|---|
C 风格 | 以 .h 结尾 | math.h | C/C++ 都可用 |
转换后的 C | 加前缀 c, 无扩展名 | cmath | C++ 可用,还能使用不是 C 的特性,例如 namespace std |
C++ 旧式风格 | 以 .h 结尾 | iostream.h | C++ 可用 |
C++ 新式风格 | 没有扩展名 | iostream | C++ 可以使用,可以使用 namespace std |
名称空间
使用 <iostream>
而非 <iostream.h>
时,应使用名称空间编译指令使 <iostream>
中的定义( cin , endl 等)对程序可用
作用:区分不同库中的同名函数,避免名称冲突
使用方式:
对象名前加名称空间
1
std::cout << "C++ is awesome.";
using namespace std;
会使 std 名称空间中的所有名称都可用,C++标准库中的函数或对象都是在命名空间 std 中定义的
可以放在函数体中,只在该函数内起作用
只使所需的名称可用
1
2using std::cout;
using std::endl;
cout: 标准输出流对象
推荐的代码风格
- 每条语句占一行
- 每个函数的开始和结束花括号单独占一行,有时开头花括号也可放于函数头行尾
- 函数中的语句都相对于花括号进行缩进
- 与函数名称相关的圆括号周围没有空白
变量
变量的命名
命名规则:
- 只能使用字母,数字和下划线,且不能以数字开头
- 区分大小写
- 不能使用关键字
- 不建议使用下划线开头
命名方案:
- 驼峰命名法:第二个单词开始首字母大写
- 帕斯卡命名法:所有单词首字母大写
- 匈牙利命名法:类型缩写做前缀,如整形n, 字符c, 字符串str/s, 布尔b, 指针p
整型
基本整型:char, short, int, long, long long
char 一般表示字符,又称小整型
short 比 int 宽度小,用于大型整数数组时可节省内存
1 字节 = 8 bit,可以表示 0~255 或 -128~127,char 只占一个字节
无符号整形:类型前面加 unsigned
,可以表示更大的值
- int 通常设为计算机的自然长度,即处理效率最高的长度
- 常见计算机的int 为4字节 32位,能够表示 -231~231-1,最大值约21亿
sizeof 运算符
运算符 sizeof
可以返回类型或变量的长度,单位为字节
- sizeof(类型名)
- sizeof 变量名 或 sizeof(变量名)
sizeof 是运算符,而不是函数
变量初始化
1 |
|
整型字面值
整型字面值即常量,通常以 10/8/16 进制表示
- 10/8/16 称为基数
- 八进制以0开头,如042; 十六进制以0x开头,如0x42;
- 只是进制的不同,可以互相运算
- cout 默认输出为10进制,例如
cout << 0x90;
会得到144 - 使用
cout << oct; //或dec,hex
指定输出时使用的进制
1 |
|
指定常量类型
整型常量默认存储为 int 类型
1
cout << "Year = " << 1492 << endl;
整数值使用后缀来显式指定类型
- 以 L/l 后缀表示 long, 如 22022L
- 以 U/u 后缀表示 unsigned int, 如 22022U
- 以 UL (不分顺序或大小写)后缀表示 unsigned long
- ( C++11 ) LL表示 long long, 以及ULL表示unsigned long long
char 类型:字符和小整数
- char 为 8bit
- 标准 ASCII码 使用7位二进制数表示 128个字符,剩下一位为0
- ASCII字符集包含大小写字母,数字,标点符号以及一些控制字符
- 大写在前,小写在后,
'A'-'a' = -32
- 可以与其他整型运算,以及类型转换,例如
int a = 'a';
,cout << char('A'+32);
ostream 类有一个成员函数 put()可以用来输出字符,例如 cout.put(‘!’)
char 字面值:
- 可以将字符用单引号括起来表示字符常量
- 实际存储的是ASCII码
- 可以使用转义序列来表示一些键盘无法输入的控制字符
- 控制字符也可放到单引号中以表示字符常量,如
'\n'
- 可以基于八进制或十六进制使用转义序列
signed char 与 unsigned char:
- char 默认情况下是否带符号是由C++实现决定的,需要时要显式设置是否有符号
- unsigned char: 0~255; signed char: -128~127
宽字符类型 wchar_t:
- wchar_t的长度和符号特征随实现而异,不能满足所有需求
- C++ 新增了具有特定长度和符号特征的类型 char16_t 和 char32_t
- char16_t 是无符号的,长16位,用前缀u表示,如
u'C'
,u"be good"
- char32_t 是无符号的,长32位,用前缀U表示,如
U'R'
,U"dirty rat"
const 限定符
声明变量时加 const 限定符可以限定变量只读
const 限定变量声明时必须初始化
1
const int Months = 12;
const 比 #define 更优,可以指明类型,可以使用作用域规则将定义限制在特定的函数或文件中,可以用于更复杂的数据类型(如数组和结构)
可以用 const 声明数组长度
浮点数
浮点数(Floating Point)是实数的近似表示
sign | exponent(8 bits) | fraction (23bits) |
---|---|---|
0 | 01111100 | 0100000000000… |
表示法:3.45E6, 8.33E-4
浮点类型
三种浮点类型:float, double, long double
- 通常 float 为32位,double 为64位, long double 为96或128位
- float 的有效数字为6-7位,double 为15-16位, long double为18-19位
浮点常量
- 默认为 double 类型
- 加f或F后缀表示以 float 类型存储,如
1.234f
,2.42E20F
- 加l或L后缀表示以 long double 类型存储,如
2.2L
整型和浮点型统称为算术(Arithmetic)类型
算术运算符
- 整数除法:
5/3
,会丢弃小数部分 - 求模运算:
5%3
,两个操作数必须为整数
类型转换
自动类型转换
自动类型转换发生的时机:
将一种算术类型的值赋给另一种算术类型的变量时
潜在的问题:
- 较大的浮点类型转换为较小的浮点类型:精度(有效数位)降低,值可能超出目标类型的取值范围,这种情况下,结果是不确定的
- 浮点类型转换为整型:小数部分丢失,可能超出范围(C++未定义结果)
- 较大的整型转换为较小的整型:可能超出范围,通常只复制右边的字节
表达式包含不同的类型时
将参数传递给参数时,实参和形参类型不一致
以{}
方式初始化时进行的类型转换(C++11):
使用大括号的初始化称为列表初始化(list-initialization)
列表初始化对类型转换的要求更严格,不允许缩窄(narrowing),例如不允许将浮点型转换为整型
使用变量进行初始化的情况:
1
2
3
4int x = 66;
long y = {x}; //allowed
char c1 = {66}; //allowed
char c2 = {x}; //警告或不允许,因为编译器认为x是一个变量,其值可能很大
表达式中的转换:
整型提升
在计算表达式时,C++将 bool, (signed/unsigned)char 和 short 值转换为 int, 例如
1
2
3short chickens = 20;
short ducks = 35;
short fow1 = chickens + ducks;在计算第三行时,C++先取得 chickens 和 ducks 的值,并把他们转换为 int, 得到的计算结果再转为 int
unsigned short 的整型提升:
如果 short 比 int 段,那么 unsigned short 转为 int;
如果 short 和 int 长度相同,那么 unsigned short 转为 unsigned int
强制类型转换
typename(value)
强制类型转换运算符:
static_cast<>
用于低风险的转换,如整型和浮点型、字符型之间的转换。不能用于不同类型指针、整型与指针之间的转换
reinterpret_cast<>
用于进行不同类型的指针之间、不同类型的引用之间和能容纳指针的数据类型之间的转换
const_cast<>
能够去除 const 属性,例:
1
2
3
4
5
6
7
8void f(int* num){cout << *num << endl;}
int main(){
const int a = 10;
f(const_cast<int*>(&a));
return 0;
}dynamic_cast
用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性
数组
数组(array) 能够存储多个同类型的值,创建之后大小固定
数组的元素(elements)在内存中依次连续存储
数组的声明:
typeName arrayName[arraySize];
- 元素数量arraySize须是整型常量、常量表达式(如 8*sizeof(int) 或const值),在编译时确定数组大小,不能是变量
- 数组是复合类型,是用其他类型创建的
元素的访问:
- 使用下标
[]
访问 - 下标从0开始,数组末尾下标为n-1
- 编译器不做下标有效性检查,注意避免越界
- 使用下标
初始化
- 局部变量不初始化,元素的值是不确定的
1
int hand(4);
- 初始化列表:
1
int cards[4] = {3, 6, 8, 10};
- 等号可省略
- 空大括号可将所有元素置为0
- 禁止缩窄(narrowing),例如
char c1 {31325}
- 注意
{1}
只会将第一个元素初始化为1,后面元素还是默认值如int()
字符串
字符串是在内存中连续存储的一系列字符
C 风格字符串
以空字符 '\0'
结尾,ASCII码为0,是字符串的结束标识
char cat[8] = {'f', 'a', 't', 'e', 's', 's', 'a', '\0'};
字符串常量
如:
"Bubbles"
字符串常量隐含结尾的空字符,用于初始化字符数组时会在末尾自动添加
'\0'
char fish[] = "Bubbles";
字符数组的大小应该为字符串长度加一
以空白字符分隔的两个字符串常量会拼接成一个
如:
cout << "I'd give my right arm to be " "a great violinist.\n"
面向行的输入:
cin.getline(array,num)
用于接收整行字符串并存入array, 读取到 num - 1 个字符,或遇到换行符时停止
- 丢弃结尾的换行符,在字符串末尾添加空字符
'\0'
- 丢弃结尾的换行符,在字符串末尾添加空字符
cin.get(array,num)
类似于
cin.getline()
,但是不丢弃结尾的换行符1
2
3cin.get(name,ArSize); //read first line
cin.get(); //read '\n'
cin.get(dessert,ArSize) //read second line
cin.get()和cin.getline() 会返回一个cin对象,因此可以连用
常用cin.get(name, Arsize).get()来读取一行
空行和失效位
cin.get()
读取空行时,会将输入输出流状态设置失效位(failbit)cin.clear()
可以重置流状态(goodbit)以恢复输入cin.rdstate()
可以查询流状态cin.getline()
遇到空行不会设置失效位,但是当读取的字符比指定的多时会设置失效位,导致后续输入被关闭
混合输入数字和字符串:
1 |
|
解决方法:
1 |
|
C 风格字符串常用库函数
头文件
<cstring>
strlen(s1)
返回字符串长度
char* strcpy(char* dest, const char* src)
复制src到dest,返回一个指向dest的指针
char* strcat(char* dest, const char* src)
将src追加到dest之后,返回一个指向dest的指针,注意长度以及最后的空字符,防止越界
int strcmp(const char* str1, const char* str2)
- 两个字符串相同函数返回零值
- 第一个字符串排在第二个字符串之前,函数返回负值
- 第一个字符串排在第二个字符串之后,函数返回正值
另外还有strncpy(),strncat(),strncmp(),使用第三个参数 n 表示复制/比较前 n 个字符
字符处理函数
头文件
<cctype>
函数 作用 isalpha 检查是否为字母 isupper 检查是否为大写 islower 检查是否为小写 isdigit 检查是否为数字 isblank 检查是否为空白字符 tolower 将字符转为大写 toupper 将字符转为小写
string 类
头文件: <string>
- string对象可以使用C风格字符串初始化
- string对象无需指定大小,可自动调整
- 可以使用下标索引法访问字符:
str[0] = toupper(str[0]);
- 字符串合并 (
+
),字符串追加 (+=
):str1 += str2
读取一行:
getline(cin,str)
常用函数(调用方式 str.fun())
函数 | 描述 | 函数 | 描述 |
---|---|---|---|
c_str | 返回字符串的不可修改的C字符数组版本 | substr | 返回子串 |
size | 返回字符数 | find | 查找子串 |
clear | 清除内容 | rfind | 从后向前查找 |
insert | 插入字符 | compare | 比较字符 |
erase | 移除字符 | empty | 检查字符串是否为空 |
append | 结尾附加字符 | replace | 替换指定部分 |
原始 (raw) 字符串
所见即所得,保留回车,不转义,允许双引号
1 |
|
- 使用前缀R和定界符,前缀R可以和其他前缀结合使用,以标识
wchar_t
等类型的原始字符串,如Ru, UR等 - 默认定界符为"(和)",如果有冲突,可在引号与括号之间添加任意基本字符作为自定义定界符
1 |
|
结构体
结构体是用户自定义的数据类型
定义结构
struct 结构体名 { 结构体成员列表 };
1 |
|
结构内初始化 ( C++ 11 ):可对所有结构内变量设初值
1 |
|
初始化
以上面的 Apple 结构体为例进行初始化
方式 1:顺序初始化
1 |
|
方式 2:指定初始化
1 |
|
方式 3:构造函数初始化:
1 |
|
注意(以下两条对方式 4 也同样适用):
- 定义了构造函数就不能使用顺序初始化和指定初始化了
- C++ 11 有构造函数时还可以使用初始化列表。例如
Apple A4 = {4, 0.5};
,这会调用构造函数,4 和 0.5 作为构造函数的实参,而非顺序初始化
方式 4:构造函数初始化列表:
- 可以初始化 const 变量
1 |
|
注意:初始化的顺序由成员变量声明的先后决定,而与构造函数初始化列表中的顺序无关。例如有以下代码:
1 |
|
预期的结果应该是 m_x = m_y = y,但实际上 m_x 先初始化,它将得到未初始化的 m_y 的值
因此,构造函数初始化列表的顺序应当和成员变量的声明顺序保持一致,避免出现此类错误
创建和使用
创建:
在定义结构体时顺便创建结构体变量
1
2
3
4
5
6struct Student
{
string name;
int age;
long long phone;
} s3; // 此处也可以初始化定义之后再创建:
Student s1;
创建时顺便初始化:
struct Student s2 = {"李四", 19, 87654321 };
注:等号可省略( C++ 11 )
使用成员访问运算符 (.
) 访问结构变量的成员
1 |
|
结构体指针
student* p = &s;
利用箭头运算符
->
可以通过结构体指针访问结构体成员p -> name
,p -> age
结构嵌套
结构变量可以作为另一个结构类型的成员
1 |
|
成员赋值
用户定义的类型与内置类型相似:
- 结构可作为参数传递给函数
- 函数可以返回一个结构
- 可以使用赋值运算符将结构赋值给同类型结构,从而拥有原结构的副本,这称为成员赋值 (memberewise assignment)
1 |
|
结构体数组
创建结构体数组:
1 |
|
给结构体中的元素赋值:
stuArr[2].age = 25;
联合体
定义和使用
1 |
|
- 联合体可在不同时刻保存不同类型的变量,但是每次只能保存一种
- 存入新值会覆盖原值
- 联合体实际是用同一块内存存储不同类型的变量,其大小为最大成员的大小
匿名联合
可在结构体内部定义,像使用结构体的成员变量一样使用它
1 |
|
枚举
1 |
|
枚举是一个带名字的整型常量的列表
枚举元素的名称必须互不相同,其值可相同
枚举元素的值是整型常量,其值默认从0开始递增
枚举元素的值可以手动指定,指定一个元素会影响后面元素的默认值
如
enum weekday {MON = 1, TUE, WED, THU, FRI, SAT = 0, SUN = 0};
此时的元素值分别为1,2,3,4,5,0,0枚举元素是整型,可被提升为 int, 而 int 不会被自动转换为枚举类型
1 |
|
错误示范
1 |
|
指针
创建和使用指针
计算机存储数据时记录的 3 种基本属性
- 信息存储的位置、存储的值、存储信息的类型
- 变量声明指出了值的类型和符号名,程序为值分配内存
- 取地址运算符 ( & ) 可以获得变量的内存地址(指针✓、const变量✓、常量×、符号常量×)
用于存储对象地址的数据类型称为指针
指针的解引用运算符 ( * ) 可以获得该地址存储的值
1
2
3
4int updates = 6;
int* p_updates = &updates;
cout << *p_updates << endl;
指针不是整型,不能将整数作为地址值直接赋给指针
const 与指针
左定值,右定向
const 在
*
左边:指针指向的变量的值不能修改const char* s
const 在
*
右边:指针的指向不能修改int* const p = &a;
const 修饰数组定义:数组的元素不能修改
const int a[5] = {1, 2, 3, 4, 5};
空指针
定义指针时应将其初始化,否则称为悬浮指针,使用它会引发无法预计的错误
初始化为空指针:
1 |
|
空指针指向的内存编号是0,而 0~255 之间的为系统占用内存,不允许用户访问,使用多次 delete 操作也不会出问题
nullptr 出现的目的是替代 NULL. 在 C++ 中 NULL 被定义为 0,NULL 即是指针,又是数字 0, 当遇到函数重载时可能会出现二义性
考虑如下函数:
1
2
3void foo(char*);
void foo(int);
void foo(double);那么
foo(NULL);
在调用哪个函数上是有歧义的
使用 new 和 delete 运算符管理动态内存
typeName* pointer_name = new typeName;
new: 在动态内存中为对象分配空间,并返回一个指向该对象的指针
- 局部变量存储在其作用域(函数)专属的栈(stack)空间内
- 栈的空间较小,默认 Win 1M, Linux 8M
- new 动态分配的内存位于堆(heap)区
- 堆的大小取决于系统(虚拟)内存,32 位系统 2~4 GB
- 计算机可能会没有足够的内存而无法满足 new 的请求
- 此时 new 会引发一个异常,返回一个空指针
使用 new 动态申请的内存用完后应当使用 delete 进行释放
- 局部变量在函数调用时创建,函数结束时销毁(回收内存),又称自动变量
- 动态申请的内存系统不会回收,如果不及时释放会造成内存泄漏 (memory leak)
- delete 后接指针用与释放指针指向的内存(new 分配的),而非删除指针本身
创建动态数组和动态结构
动态数组和动态联编
- 程序运行时通过 new 创建的数组称为动态数组 (dynamic array)
- 动态数组的长度在运行时确定,确定后大小不可变
- 通过声明创建的数组,编译时确定数组大小,并为数组分配内存,称为静态联编 (static binding)
使用 new 创建动态数组
type_name* ptr = new type_name[num_elements];
- 分配大小为
sizeof(type_name) * num_elements
的连续内存,并返回首地址,赋给指针 ptr - 不能通过 sizeof 运算符获取动态数组的大小
- 分配大小为
使用
delete[]
释放内存- new 分配的内存务必使用 delete 释放
- delete 只应释放 new 分配的内存
- new 分配的内存只能使用 delete 释放一次
- new[] 为动态数组分配的内存,应使用 delete[] 释放
- 对空指针使用 delete 是安全的
指向数组的指针支持与整数的算术运算 (+, -), 以在数组元素间移动
使用 malloc/free 分配/释放内存
头文件:<stdlib.h> /* malloc, free, srand, rand */
例:随机字符串生成
1 |
|
智能指针简介
C++ 标准库 <memory>
提供 auto_ptr 等智能指针管理动态对象
- 智能指针行为类似常规指针,但可以通过析构函数自动释放所指向的对象
C++ 11 引入了 shared_ptr, unique_ptr, weak_ptr
- 其中 shared_ptr 采用引用计数,允许多个智能指针指向同一对象
- unique_ptr 独占所指向的对象
指针、数组和指针算术运算
- 数组名代表数组首元素的地址
- 使用指针比使用数组下标的程序执行速度略快
1 |
|
将指针 ip 所指对象值加一
++*ip
或(*ip)++
数组名和指针的区别
p = a
和p++
合法,a = p
和a++
非法- sizeof 结果不一样
有效的指针运算:
- 同类型指针之间的赋值运算
- 指针与整数之间的加减运算:按所指对象的大小偏移
- 指向同一数组的两个指针之间的比较和减法运算:
若 p < q,那么q - p - 1
就是 p 和 q 之间元素的数目 - 用字面量 0 给指针赋值,或指针与 0 之间的比较
指针数组与指向数组的指针
指针数组:
int* p1[5];
,p1 是一个数组,它的每个元素都是指针指向数组的指针:
int (*p2)[5];
p2 是一个指针,指向一个(具有5个整型元素的)数组。p2 + 1
运算会使 p2 偏移 5 个整型长度
指针与二维数组
对于数组 int a[4]
,可定义指针 int* p = a
,然后使用指针 p 来访问数组元素
对于二维数组 int a[2][3]
,逻辑上可以看作:
行号\列号 | 0 | 1 | 2 |
---|---|---|---|
a[0] | a[0][0] | a[0][1] | a[0][2] |
a[1] | a[1][0] | a[1][1] | a[1][2] |
可见 a[0], a[1] 都是包含三个元素的数组,a 每偏移 1 个单位,就相当于偏移了 3 个整型元素的长度
因此,数组名 a 对应于一个指向(包含三个元素的)数组的指针 int (*p)[3] = a
, 访问数组的元素时 *(*(p + i) + j)
,a[i][j]
, p[i][j]
三者是等价的
二维数组一维化:
数组元素在内存中是连续存储的,因此只需要取到首元素的地址,然后就可以按照一维数组的方式使用
a[0][0] | a[0][1] | a[0][2] | a[1][0] | a[1][1] | a[1][2] |
---|
例:
1 |
|
指针和字符串
在 cout 和多数 C++ 表达式中,char 数组名,char 指针和引号中的字符串常量都被解释为字符串首字符地址
- C++ 中的字符串常量是
const char*
类型 - cout 可以直接输出指针的值,但是字符指针除外,因为会输出它指向的字符串
- 要输出字符指针的值,需要强制类型转换
cout << (void*)s;
- 字符串赋给数组也应使用
strcpy()
或strncpy()
,而不是赋值运算符
使用 new 创建动态结构
inflatable *ps = new inflatable;
要使用指针访问成员变量,需要箭头成员运算符 ->
ps -> price
相当于 (*ps).price
自动存储、静态存储和动态存储
三种管理数据内存的方式:自动存储、静态存储和动态存储
自动存储
- 在函数内部定义的常规变量使用自动存储空间,称为自动变量
- 自动变量在所属函数调用时创建,函数结束时自动销毁(回收内存)
- 自动变量又称局部变量,其作用域为包含它的代码块
- 自动变量通常存储在栈(stack)区,栈是后进先出(LIFO)的
静态存储
整个程序执行期间都存在的存储方式
使变量成为静态存储的两种方式:在函数外部定义变量(全局变量),变量声明时使用 static 关键字(静态变量)
static double fee;
动态存储
- new 分配的内存位于程序的自由存储空间或堆区(heap)
- 数据(变量)的生命周期不受函数或程序的生存时间控制
- 使用 new 和 delete 让程序员控制动态存储的数据
- new 分配的内存不连续,跟踪新分配内存的位置比较困难
vector 和 array 简介
- 头文件
<vector>
和<array>
声明:
vector<typeName> vectorName(n_elem);
array<typeName, n_elem> arrayName;
vector 可以通过 push_back()
增添元素;array 不可以改变长度,且 array 的大小 n_elem 不能是变量