数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。
【整数类型】
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 字节 | -128 ~ 127 或 0 ~ 255 |
unsigned char | 1 字节 | 0 ~ 255 |
signed char | 1 字节 | -128 ~ 127 |
int | 2 或 4 字节 | -32,768 ~ 32,767 或 -2,147,483,648 ~ 2,147,483,647 |
unsigned int | 2 或 4 字节 | 0 ~ 65,535 或 0 ~ 4,294,967,295 |
short | 2 字节 | -32,768 ~ 32,767 |
unsigned short | 2 字节 | 0 ~ 65,535 |
long | 4 字节 | -2,147,483,648 ~ 2,147,483,647 |
unsigned long | 4 字节 | 0 ~ 4,294,967,295 |
【浮点类型】
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 字节 | 1.2E-38 ~ 3.4E+38 | 6 位小数 |
double | 8 字节 | 2.3E-308 ~ 1.7E+308 | 15 位小数 |
long double | 16 字节 | 3.4E-4932 ~ 1.1E+4932 | 19 位小数 |
C 没有从语法上支持“真”和“假”,只是用 0 false
和 非0 true
来表示。
C++ 新增了 bool 类型(布尔类型),占用 1 个字节,只有两个取值,true 和 false。
void 类型指定没有可用的值。它通常用于以下三种情况下:
void exit (int status);
。void
类型的参数。例如 int rand(void);
。void *
的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc(size_t size);
返回指向 void
的指针,可以转换为任何数据类型。定长声明一个数组需要指定元素的类型和元素的数量。
// type可以是任意有效的数据类型,arraySize必须是一个大于零的整数常量
type arrayName [arraySize];
// 例如:
double balance[5];
这种方式声明的数组,占用栈内存空间,如果数组长度过大,可能会导致栈内存溢出。
声明时初始化
double balance[] = {
1000.0, 2.0, 3.4, 7.0, 50.0};
声明数组的大小之后,无法更改,但有时数组大小可能不够,就需要动态扩容,即动态内存分配。动态分配的内存占用堆内存空间,因此使用完毕后应主动释放。
内存分配(memory allocation)。 malloc()
函数保留指定字节数的内存块,返回一个 void *
可类型转换为任何形式的指针。
// type可以是任意有效的数据类型,size是一个需要保留的内存块大小
type *ptr = (type *) malloc(size);
// 例如:
int *ptr = (int *) malloc(100 * sizeof(int));
// 但是此时指针数组尚未初始化,需要手动初始化
// 将ptr中当前位置后面的n个字节用0替换并返回ptr。
memset(ptr, 0, 100 * sizeof(int));
连续分配(contiguous allocation)。malloc()
函数分配内存,但不初始化内存。而 calloc()
函数分配内存并将所有位初始化为0。
// type可以是任意有效的数据类型,分配count个,每个size字节的连续内存块
type *ptr = (type *) calloc(count, size);
// 例如:
int *ptr = (int *) calloc(100, sizeof(int));
重新分配(reset allocation)。重新分配内存大小,有两种情况:
// type可以是任意有效的数据类型,分配count个,每个size字节的连续内存块
type *ptr = (type *) realloc(ptr, newsize);
// 例如:
ptr = (int *) realloc(ptr, 200 * sizeof(int));
使用 calloc()
或 malloc()
创建的动态分配内存,必须明确使用 free()
释放空间。
free(ptr);
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。
int i = 10;
// p的值是i的内存地址
int *p = &i;
std::cout << "i:" << i << std::endl; // 【10】
std::cout << "&i:" << &i << std::endl; // 【0x7ffee46bba38】
std::cout << "p:" << p << std::endl; // 【0x7ffee46bba38】
std::cout << "&p:" << &p << std::endl; // 【0x7ffee46bba30】
// 解引用,解析p指向地址所存储的值
std::cout << "*p:" << *p << std::endl; // 【10】
即数组的指针,数组名本身就是数组的首地址。因此 int *p1 = array1;
和 int (*p2)[3] = array2;
都是指向数组的指针,即数组指针。
// 声明并初始化一维数组
int array1[] = {
11, 22, 33, 44, 55, 66};
// 声明指针p1,并指向数组的首地址
int *p1 = array1;
// 打印指针p1指向的地址值
std::cout << "p1:" << p1 << std::endl; // 【0x7ffeed1d5a20】
// 打印指针p1指向的地址所存储的值,即数组第0元素
std::cout << "*p1:" << *p1 << std::endl; // 【11】
// 打印指针p1指向的地址的下1位地址所存储的值,即数组第1元素
std::cout << "*(p1 + 1):" << *(p1 + 1) << std::endl; // 【22】
// 打印指针p1指向的地址的下6位地址所存储的值,即数组第6元素,越界,值不可确定
std::cout << "*(p1 + 6):" << *(p1 + 6) << std::endl; // 【1485046000】
std::cout << "------------------------" << std::endl;
// 声明并初始化二维数组
int array2[2][3] = {
{
11, 22, 33}, {
44, 55, 66}};
// 声明指针p2,并指向二维数组的首地址
int (*p2)[3] = array2;
// 打印指针p2指向的地址值
std::cout << "p2:" << p2 << std::endl; // 【0x7ffeed1d5a00】
// 打印指针p2指向的地址所存储的值,即二维数组第0行的首地址值
std::cout << "*p2:" << *p2 << std::endl; // 【0x7ffeed1d5a00】
// 打印指针p2指向的地址所存储的值,即二维数组第0行第0元素
std::cout << "**p2:" << **p2 << std::endl; // 【11】
// 打印指针p2指向的地址的下1位地址所存储的值,即二维数组第1行的首地址值
std::cout << "*(p2 + 1):" << *(p2 + 1) << std::endl; // 【0x7ffeed1d5a0c】
// 打印指针p2指向的地址的下1位地址所存储的值,即二维数组第1行第0元素
std::cout << "**(p2 + 1):" << **(p2 + 1) << std::endl; // 【44】
// 打印指针p2指向的地址的下1位地址所存储的值的下1位地址所存储的值,即二维数组第1行第1元素
std::cout << "*(*(p2 + 1) + 1):" << *(*(p2 + 1) + 1) << std::endl; // 【55】
即指针类型的数组,数组中的每个元素都是指针类型。
int i = 10;
int j = 20;
int k = 30;
// 声明并初始化一个指针数组
int *p_array[] = {
&i, &j, &k};
std::cout << "p_array:" << p_array << std::endl; // 【0x7ffeea87ba20】
std::cout << "*p_array:" << *p_array << std::endl; // 【0x7ffeea87ba18】
std::cout << "**p_array:" << **p_array << std::endl; // 【10】
std::cout << "*(p_array + 1):" << *(p_array + 1) << std::endl; // 【0x7ffeea87ba14】
std::cout << "**(p_array + 1):" << **(p_array + 1) << std::endl;// 【20】
数据类型前用 const
修饰,被定义的指针变量就是指向常量的指针,即常量指针。
const type * p
或 type const * p
。// 定义一个常量指针
const char *p = "OK";
// ❎ 编译报错:修改指针所指向的内存区域的值
p[0] = 'A';
// ✅ 编译通过:修改指针的指向
p = "YES";
// ✅ 编译通过:修改指针的指向
p = NULL;
指针变量前用 const
修饰,被定义的指针变量就是指针类型的常量,即指针常量。
type * const p
。// 定义一个指针常量
const * char p = "OK";
// ✅ 编译通过:修改指针所指向的内存区域的值
p[0] = 'A';
// ❎ 编译报错:修改指针的指向
p = "YES";
// ❎ 编译报错:修改指针的指向
p = NULL;
数据类型前用 const
修饰,指针变量前也用 const
修饰,被定义的指针变量就是常量指针类型的常量,即常指针常量。
const type * const p
或 type const * const p
。// 定义一个指针常量
const * char p = "OK";
// ❎ 编译报错:修改指针所指向的内存区域的值
p[0] = 'A';
// ❎ 编译报错:修改指针的指向
p = "YES";
// ❎ 编译报错:修改指针的指向
p = NULL;
当指针变量所指的变量也是一种指针时,则该指针变量是一种指向指针的指针,即多级指针。
int i = 10;
int *p1 = &i; // 一级指针
int **p2 = &p1; // 二级指针
int ***p3 = &p2; // 三级指针
std::cout << "i:" << i << std::endl; // 【10】
std::cout << "*p1:" << *p1 << std::endl; // 【10】
std::cout << "**p2:" << **p2 << std::endl; // 【10】
std::cout << "***p3:" << ***p3 << std::endl;// 【10】
函数是一组一起执行一个任务的语句。
函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
返回值类型 函数名(参数列表);
函数函数定义的一般形式如下:
返回值类型 函数名(参数列表)
{
函数体
}
函数由一个函数头和一个函数主体组成:
void
。把参数的实际值赋值给函数的形式参数,因此,修改形式参数对实际参数没有影响。
int fun(int x) {
return x += 10; }
int main() {
int num = 10;
// 值传递
fun(num);
// 不会改变本身的实际参数的值
std::cout << "num:" << num << std::endl; // 【10】
return 0;
}
把参数的地址赋值给函数的形式参数。在函数内,可以通过该地址访问实际参数。因此,修改形式参数对实际参数有影响。
int fun(int *x) {
return *x += 10; }
int main() {
int num = 10;
// 指针传递,把实际参数的地址传给形式参数中的指针
fun(&num);
std::cout << "num:" << num << std::endl; // 【20】
return 0;
}
把参数的引用赋值给函数的形式参数。在函数内,可以通过该引用访问实际参数。因此,修改形式参数对实际参数有影响。
int fun(int &x) {
return x += 10; }
int main() {
int num = 10;
fun(num);
std::cout << "num:" << num << std::endl; // 【20】
return 0;
}
使用到四个宏:va_list
、va_start(va_list, arg)
、va_arg(va_list, type)
、va_end(va_list)
。
/**
* @param count 参数个数
*/
int sum(int count, ...) {
// 声明一个va_list变量,arg代表...
va_list arg;
// 开始使用可变参数
va_start(arg, count);
int sum = 0;
for (int i = 0; i < count; ++i) {
// 获取可变参数
sum += va_arg(arg, int);
}
// 清理可变参数
va_end(arg);
return sum;
}
int main() {
int sum_num = sum(5, 1, 1, 1, 1, 1);
std::cout << "sum:" << sum_num << std::endl;
return 0;
}
缺点: 不安全!count 如果和实际参数个数不一致,就会发生严重错误。
initializer_list
头文件,且元素必须具有相同类型。initializer_list
模板代表可变参数列表。initializer_list
中的参数。#include
#include
int sum(std::initializer_list<int> args) {
int sum = 0;
// 有点像迭代器
for (auto arg = args.begin(); arg != args.end(); arg++) {
sum += *arg;
}
return sum;
}
int main() {
// 实参必须全部放到{}中
int sum_num = sum({
1, 1, 1, 1, 1});
std::cout << "sum:" << sum_num << std::endl;
return 0;
}
返回值是一个指针的函数。
声明格式:返回值类型 *函数名(参数列表)
,*
与前者结合,使得返回值是一个指针类型。
int *sum(int num_1, int num_2) {
// 基本数据类型,默认都在栈上分配内存
int sum = num_1 + num_2;
return ∑
}
int main() {
int *sum_p = sum(10, 20);
std::cout << "*sum_p:" << *sum_p << std::endl; // 【1】
return 0;
}
⭐️ 注意:int sum = num_1 + num_2;
局部变量会在栈上分配,当 int *sum()
函数运行结束,sum
就会被销毁,但它的内存地址却作为返回值保留了。因此,main
函数再获取原地址所对应的值时,得到的值就不再是正确结果了。
不同的编译平台,对销毁局部变量的处理不同:
为了能让外部函数能够通过地址获取到正确值,那么局部变量就不能栈上分配,可以放到堆中。
int *sum(int num_1, int num_2) {
// 在堆上分配内存
int *sum = static_cast<int *>(malloc(sizeof(int)));
// 对该内存空间赋值
sum[0] = num_1 + num_2;
return sum;
}
int main() {
int *sum_p = sum(10, 20);
std::cout << "*sum_p:" << *sum_p << std::endl; // 【30】
free(sum_p);
return 0;
}
指向函数的指针。
声明格式:返回值类型 (*函数名)(参数列表)
,*
强制与函数名结合,使得函数名是一个指针。
// 普通函数
int add(int x, int y) {
return x + y; }
// 普通函数
int sub(int x, int y) {
return x - y; }
int main() {
// 声明一个函数指针,返回值为int,接受两个int型参数
int (*fun)(int, int);
// 将函数指针指向add函数,或者fun = &add;
fun = add;
// 两种调用函数的方法
std::cout << "(*fun)(1,2)=" << (*fun)(1, 2) << std::endl; // 【3】
std::cout << "fun(1,2)=" << fun(1, 2) << std::endl; // 【3】
// 将函数指针指向sub函数,或者fun = ⊂
fun = sub;
// 两种调用函数的方法
std::cout << "(*fun)(5,3)=" << (*fun)(5, 3) << std::endl; // 【2】
std::cout << "fun(5,3)=" << fun(5, 3) << std::endl; // 【2】
return 0;
}
把函数指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。
// 定义两种函数指针类型
typedef void(*Success)(char *);
typedef void(*Failure)(char *);
// 执行函数
void http(char *url, Success success, Failure failure) {
// 通过函数指针指向真实函数
if (true){
success("请求成功");
} else{
failure("请求失败");
}
}
// 回调函数
void success(char *msg) {
printf(msg);
}
// 回调函数
void failure(char *msg) {
printf(msg);
}
int main() {
// 执行函数,并把回调函数传递进去
http("https://www.qq.com", success, failure);
return 0;
}
预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。
所有的预处理器命令都是以井号(#)开头。
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
用于创建符号常量,该符号常量通常称为宏。
#include
using namespace std;
// 告诉编译器,把所有的PI替换为3.14159。
#define PI 3.14159
// 取消已经定义过的PI宏
#undef PI
// 告诉编译器,把所有的PI替换为3.141592653。
#define PI 3.141592653
int main () {
cout << PI << endl;
// 编译后的代码:
// cout << 3.141592653 << endl;
return 0;
}
也可以定义一个带有参数的宏。
#include
using namespace std;
// 告诉编译器,把所有的MIN(a,b)替换为(a
#define MIN(a,b) (a
int main () {
int i = 10;
int j = 20;
cout << MIN(i, j) << endl;
// 编译后的代码:
// cout << i
return 0;
}
注意,宏只做文本替换,不做任何校验。
#include
using namespace std;
#define FUN(a, b) a*b
int main() {
// 期望结果:(1+2)*(3+4) = 21
// 实际结果:1 + 2*3 + 4 = 11
cout << FUN(1 + 2, 3 + 4) << endl; // 【11】
return 0;
}
有选择地对部分程序源代码进行编译,称为条件编译。
如果满足条件,就编译:
#if 0
// 不参与编译,相当于注释
#endif
如果宏没有定义,则新定义一个:
#ifndef MESSAGE
#define MESSAGE "Hello World!"
#endif
如果宏已经定义,则取消这个宏:
#ifdef MESSAGE
#undef MESSAGE
#endif
宏 | 描述 |
---|---|
DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
#include
using namespace std;
int main() {
cout << __LINE__ << endl; // 【5】
cout << __FILE__ << endl; // 【/Users/admin/workspace/CppTest/main.cpp】
cout << __DATE__ << endl; // 【Mar 4 2021】
cout << __TIME__ << endl; // 【14:50:02】
cout << __STDC__ << endl; // 【1】
return 0;
}
① #
运算符会把令牌转换为用引号引起来的字符串。
#include
using namespace std;
// ① 告诉编译器,把所有的MKSTR(x)替换为#x。
// ② 把传入的x转换为x的字符串
#define MKSTR(x) #x
int main() {
cout << MKSTR(HELLO C++); // 【HELLO C++】
// cout << "HELLO C++";
return 0;
}
② ##
运算符用于连接两个令牌。
#include
using namespace std;
// ① 告诉编译器,把所有的concat(a, b)替换为a ## b。
// ② 把传入的a和b转换为ab
#define concat(a, b) a##b
int main() {
int xy = 100;
cout << concat(x, y); // 【100】
// cout << xy;
return 0;
}
结构体是用户自定义的可用的数据类型。
声明格式:
struct 结构体名称 {
成员类型1 成员名称1;
成员类型2 成员名称2;
成员类型3 成员名称3;
};
结构体名称 变量名称;
使用成员访问运算符 .
来访问结构体成员。
#include
using namespace std;
// 定义一个结构体类型User
struct User {
int age; // 成员1
char *name; // 成员2
};
int main() {
User user1;
User user2;
// 给成员赋值
user1.age = 20;
user1.name = "张三";
user2.age = 30;
user2.name = "李四";
// 获取成员的值
cout << user1.name << " " << user1.age << endl;
cout << user2.name << " " << user2.age << endl;
return 0;
}
在 64 位系统中,int
占用4个字节,char *
占用8个字节,因此 User
理论大小为12个字节。
struct User {
int age;
char *name;
};
cout << sizeof(User) << endl; // 【16】
实际上 User
的大小为16个字节。
【字节对齐的步骤】
int age
的起始内存地址为 0x0000
,则结束内存地址为 0x0003
,共占用4个字节。char *name
的理论起始地址为 0x0004
,但是 char *
类型占8个字节,因此需要对 0x0000
做8个字节的最小整数倍偏移而得到起始地址,即 0x0008
,则结束内存地址为 0x0011
。int
和 char *
对比,最大的是 char *
占用8个字节。0x0000
到 0x0011
总共占用16字节,恰好是8的整数倍,因此结构体不用再做对齐。【字节对齐的意义】
【指定对齐大小】
# pragma pack(2) // 指定以2个字节来对齐,必须是2的整数倍
struct User {
int age;
char *name;
};
# pragma pack() // 还原为系统默认的对齐大小
cout << sizeof(User) << endl; // 【12】
共用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。
定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
声明格式:
union 共用体名称 {
成员类型1 成员名称1;
成员类型2 成员名称2;
成员类型3 成员名称3;
};
共用体名称 变量名称;
使用成员访问运算符 .
来访问共用体成员。
#include
using namespace std;
// 定义一个共用体类型U
union U {
int i; // 成员1
int j; // 成员2
};
int main() {
U u;
u.i = 10; // 对成员1赋值
u.j = 20; // 对成员2赋值
cout << u.i << " " << u.j << endl; // 【20 20】任何时候,只有一个成员有正确值
cout << sizeof(U) << endl; // 【4】占用的内存 = 共用体中最大成员占用的内存
return 0;
}
C 语言字符串实际上是使用 null
字符 \0
终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。
char str1[6] = {
'H', 'e', 'l', 'l', 'o', '\0'}; // 以\0结尾的字符数组
char str2[] = "Hello"; // 数组初始化规则,自动补充\0
char *str3 = "Hello"; // 指向数组其实地址的指针,自动补充\0
字符数组操作函数:
函数 | 结果 |
---|---|
strcpy(s1, s2); | 复制字符串 s2 到字符串 s1。 |
strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号。 |
strlen(s1); | 返回字符串 s1 的长度。 |
strcmp(s1, s2); | 如果 s1 和 s2 是相同的,则返回 0;如果 s1 |
strchr(s1, c); | 返回一个指针,指向字符串 s1 中字符 c 的第一次出现的位置。 |
strstr(s1, s2); | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
C++ 标准库提供了 string
类类型,支持上述所有的操作,另外还增加了其他更多的功能。
string str1 = "Hello"; // 使用字面量创建
string str2(str1); // 使用构造方法创建
string str3("Hello"); // 使用构造方法创建
string *str4 = new string("Hello"); // 在堆中申请内存创建
string str5 = str1; // 字符串赋值
string str6 = str1 + str2; // 字符串拼接
cout<< str3 << endl; // 输出一般类型的字符串
cout<< str4->c_str() << endl; // 输出指针类型的字符串
delete str4; // 用完了要释放堆内存