本帖最后由 委员长 于 2024-12-29 13:58 编辑
C++ 定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型,这些变量存储在堆或者栈中。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
基本内置类型
算术类型
数据类型的大小取决于编译器。注意,定义 float 类型的变量时要在变量后面加上 f
。
类型 |
含义 |
最小尺寸 |
bool |
布尔类型 |
未定义 |
char |
字符 |
8 位 |
wchar_t |
宽字符 16 |
_位 |
char16_t |
Unicode 字符 16 |
_位 |
char32_t |
Unicode 字符 32 |
_位 |
short |
短整型 16 |
_位 |
int |
整型 16 |
_位 |
long |
长整型 32 |
_位 |
long long |
长整型 64 |
_位 |
float |
单精度浮点数 |
6-7 位有效数字 |
double |
双精度浮点数 10 |
15-16 位有效数字 |
long double |
扩展精度浮点数 10 |
_位有效数字 |
如果我们想知道当前编译器为每个类型分配多大的字节,可以用 sizeof 关键字。
#include<iostream>
int main() {
// 1
std::cout << sizeof(bool)<< std::endl;
}
类型转换
基本的类型转换
#include<iostream>
using namespace std;
int main(){
bool b = 42; // b 为 true
int i = b; // i 为 1
i = 3.14; // i 为 3
double p1 = i; // p1 为 3
unsigned char c = -1; // 假设 char 占 8bit,c 的值为255
signed char c2 = 256; // 假设 char 占 8bit,c 的值是未定义
cout<<"b="<<b<<"\n"
<<"i="<<i<<"\n"
<<"p1="<<p1<<"\n"
<<"c="<<int(c)<<"\n"
<<"c2="<<c2<<"\n";
return 0;
}
由于不同的操作系统数据类型的表现能力不一样,如某些 OS int 是 4 字节,有些则不是 4 字节。因此在进行编程的时候要避免无法预知和依赖于实现环境的行为。
含无符号类型的表达式
如果一个有符号的 int 和无符号的 int 进行运算,最后的数据类型会被提升为无符号数据。
void test2(){
int a = -1;
unsigned int b = 0;
cout<<a+b<<endl; // 4294967295
}
如果一个有符号的 long 和一个无符号的 int 进行运行,最后无符号数据类型会被提升为 long。
void test2(){
long a = -1;
unsigned int b = -1;
// b 原先是 1000 ... 0001
// 提升为 long 后 0000 ... 1000 ... 0001
cout<<a+b<<endl; // 4294967294
}
习题,读程序,说结果
void test3(){
// 42 = 32+8+2
unsigned u = 10,u2 = 42;
// 0000 ... 0000 0000 1010
// 0000 ... 0000 0010 1010
// 0000 ... 0000 0010 1001
cout<< u2-u <<endl; // 32
// 10-42 = -32
// 借位做减法
// 0000 ... 0000 0000 1010
// 0000 ... 0000 0010 1010
// 1111 ... 1111 1110 0000
// 1000 ... 0000 0010 0000
// 负数的补码 = 取反+1(无符号的运算最后还是无符号)
// 1111 ... 1111 1101 1111 + 1
// 最后当成无符号数来算出他的值
// 1111 ... 1111 1110 0000
cout<< u-u2 <<endl;
int i = 10,i2 = 42;
cout<< i2-i <<endl;
cout<< i-i2 <<endl;
cout<< i-u <<endl;
cout<< u-i <<endl;
}
字面值常量
整型和浮点型字面量
以 0 开头的整数代表八进制数,以 0x 或 0X 开头的代表十六进制数。
void test4(){
// 4*1+2*8
// 0 开头八进制
cout<<024<<endl;
// 0x 开头十六进制
// 1*16+2 = 18
cout<<0x12<<endl;
}
浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用 E 或 e 标识:
3.14 3.14e0 0. 0e0 .001
注意:默认的,浮点型字面值是一个 double。
字符和字符串字面量
由单引号括起来的一个字符称为 char 型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
'a' // 字符字面量
"Hello" // 字符串字面值
指定字面量的类型
什么叫指定字面量的类型呢?比如你把字面量 char 指定为 wchar_t 类型的。
指定方式 |
说明 |
L'a' |
宽字符型字面量,类型是 wchar_t |
u8"hi!" |
utf-8 字符串字面值 (utf8 用 8 位编码一个 Unicode 字符) |
42ULL |
无符号整型字面值,类型 unsigned long long |
1E-3F |
|
3.14158L |
long double 类型 |
取整规则
向零取整。
#include<iostream>
using namespace std;
int main() {
double dv = 10.23;
// 10
cout << (int) dv << endl;
}
int 类型的除法也是向零取整
#include<iostream>
using namespace std;
int main() {
int n1 = 5;
int n2 = -5;
// 向 0 取整
cout << n1 / 2 << endl; // 2.5 --> 2
cout << n2 / 2 << endl; // -2.5 --> -2
}
变量
变量定义
定义的方式和其他语言类似
int a=0,b=0;
初始值
C++ 中,初始化是一个异常复杂的问题,在 C++ 中,初始化和赋值是两个完全不同的操作。
列表初始化
void test5(){
int n = 10;
// 以下为列表初始化。
int n1 = {10};
int n2{10};
int n3(10);
// 10:10:10
cout<<n1<<":"<<n2<<":"<<n3<<endl;
}
<span style="color:orange">列表初始化的好处:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。</span>
void test5(){
double n = 10.123;
int n1 = {n};
int n2{n};
int n3(n);
// g++ cast.cpp -o a -std=c++11 会报错
cout<<n1<<":"<<n2<<":"<<n3<<endl;
}
/*
cast.cpp: In function ‘void test5()’:
cast.cpp:69:16: warning: narrowing conversion of ‘n’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
int n1 = {n};
^
cast.cpp:70:13: warning: narrowing conversion of ‘n’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
int n2{n};
*/
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了“默认值”。<span style="color:orange">函数外部的变量会有默认初始化,而函数内部的变量没有默认初始化!如果试图拷贝或以其他形式访问此类值将引发错误。</span>
int out;
void test6(){
int inner; // 没有默认初始化
cout<<out<<":"<<inner<<endl;
}
// 0:21845
练习题
int i = {3.14}; // C++11 报错
int i = 3.14; // C++11 不报错
变量声明和定义
为了允许把程序拆分成多个逻辑部分来编写,C++ 支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
如果将程序分为多个文件,则需要有在文件间共享代码的方法。例如,一个文件的代码可能需要使用另一个文件中定义的变量。如 std::cout
和 std::cin
,它们定义于标准库,却能被我们写的程序使用。
为了支持分离式编译,C++ 将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。<span style="color:red">而定义(definition)负责创建与名字关联的实体。</span>
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
<span style="color:red">如果想声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显式地初始化变量</span>
extern int i; // 声明 i 而非定义 i, 说明此变量 i 在别处定义的,要在此处引用
extern double pi = 3.14; // 声明并定义 pi
int j; // 声明并定义 j
在函数体内部,如果试图初始化一个由 extern 关键字标记的变量,将引发错误。
使用 extern 引用其他文件的变量
// var.cpp
#include <string.h>
using namespace std;
const char *str = "var.cpp 的变量";
// init.cpp
#include <iostream>
#include "var.cpp"
using namespace std;
// 引用外部变量。
extern const char *str;
int main(){
cout<<str<<endl;
return 0;
}
变量只能被定义一次,但是可以被声明多次。这样我们就可以将声明和定义分离。变量的定义只出现在一个文件中,需要使用到这个变量的文件就声明变量。
作用域
和其他语言一样,不赘述。
习题
#include<iostream>
using namespace std;
int i = 42;
int main(){
int i = 100;
int j = i;
// 100
cout<<j<<endl;
return 0;
}
#include<iostream>
using namespace std;
int main(){
int i = 100, sum = 0;
// for 循环内部的 i 是块级作用域。而在 Java 中会报错。
for(int i=0; i!=10; ++i)
sum+=i;
cout<<i<<":"<<sum<<endl;
return 0;
}
// 100:45
复合类型
复合类型(compound type)是指基于其他类型定义的类型。C++ 有几种复合类型,本章将介绍其中的两种:引用和指针。
引用
引用(reference)为对象起了另外一个名字。
#include<iostream>
using namespace std;
int main(){
int val = 1024;
int &refVal = val;
// int &refVal2; // 报错,引用在定义时必须被初始化
refVal = 10;
cout<<val<<endl; // 10
return -1;
}
定义引用时,程序把引用和它的初始值绑定(bind)在一起。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。由于无法让引用重新绑定到另外一个对象,因此引用必须初始化。
注意:引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。因为引用本身不是一个对象,所以不能定义引用的引用。
#include<iostream>
using namespace std;
int main(){
int &refVal = 10; // 报错,引用类型的初始值必须是一个对象
double dVal = 3.1;
int &refValI = dVal; // 报错,引用类型的初始值必须一致,此处必须是 int 型对象
}
引用定义
#include<iostream>
using namespace std;
int main(){
// ri 是一个引用,与 i 绑定在了一起
int i, &ri = i;
i = 5;
ri = 11; // 为 i 重新赋值了
// 11, 11
cout<<i<<" "<<ri<<endl;
return 0;
}
指针
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也会有一个不确定的值。
指针的定义
#include<iostream>
using namespace std;
int main(){
int *p1,*p2; // p1,p2 都是指针
double dp1,*dp2; // dp2 是指针
p1 = new int; // 为指针 p1 分配内存空间
*p1 = 1; // *p1 解指针,将指针指向的内存空间中的值写为 1
cout<<*p1<<endl; // *p1 拿到指针指向的内存空间中的值
}
指针存的是地址值,所以可以把其他变量的地址值赋值给对应类型的指针。
#include<iostream>
using namespace std;
int main(){
int number = 10;
int *p = &number; // 将 number 的内存地址赋值给指针 p
*p = 20; // 修改指针 p 所指向内存地址中的值,即修改了 number 的值
cout<<number<<endl; // 输出 20
}
指针的赋值类型需要匹配,否则会报错
#include<iostream>
using namespace std;
int main(){
int number = 10;
// error: cannot convert ‘int*’ to ‘double*’ in initialization
double *p = &number;
}
指针的值
指针的值(即地址)应属下列 4 种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
利用指针访问对象
如果指针指向了一个对象,允许使用解引用符(操作符 *
)来访问该对象。
#include<iostream>
using namespace std;
int main(){
int number = 10;
int *p = &number; // 将 number 的内存地址赋值给指针 p
// *p 所指向的对象就是 number,修改 number 对象的值
*p = 20; // 修改指针 p 所指向内存地址中的值,即修改了 number 的值
cout<<number<<endl; // 输出 20
}
空指针
空指针不指向任何对象。生成空指针的方式如下:
#include<iostream>
using namespace std;
int main(){
// 下面三个都是生成空指针
int *p1 = nullptr;
int *p2 = NULL;
int *p3 = 0;
}
其他指针操作
任何非 0 指针对应的条件都是 true。
#include<iostream>
using namespace std;
int main(){
int *p1 = new int;
int *p2 = 0;
if(p1){
cout<<"p1 not zero"<<endl;
}
if(p2){
cout<<"p2 not zero"<<endl;
}
}
// p1 not zero
void* 指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个 void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解;使用 void * 可以实现多态。
练习
#include<iostream>
using namespace std;
int main(){
int n = 10;
int *p = &n;
*p = *p * *p;
// 100
cout<<*p<<endl;
return 0;
}
int main(){
int i=0;
double *dp = &i; // 报错,类型不一致
int *ip = i; // 报错,要给地址值
int *p = &i;
}
下列代码为什么 p 合法而 lp 非法
int i = 42;
// 合法,void 类型指针,可以接收所有类型的
void *p = &i;
// 不合法,不能将 int 类型的指针赋值给 long 类型的
// cannot convert ‘int*’ to ‘long int*’
long *lp = &i;
理解符合类型声明
在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量。
#include <iostream>
using namespace std;
int main(){
// i 是一个 int 类型的数,p 是一个 int 型指针,r 是一个 int 型引用
int i = 1024, *p = &i, &r = i;
*p = 100;
cout<<i<<endl;
r = 1001;
cout<<i<<endl;
return 0;
}
定义多个变量
很多人容易误认为在定义语句中,类型修饰符(*或 &)作用于本次定义的全部变量,其实不是的,它只会作用于它修饰的一个变量。
// 这样非常容易误认为 p1 p2 都是指针类型
// 实际上只有 p1 是指针类型
int* p1,p2
建议写成这样,将变量和类型修饰符(*或 &)写在一起。
int *p1, *p2;
<b style="color:red">二级指针</b>
二级指针,指向指针的指针。
<span style="color:orange">一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。</span>
int val = 10;
int *pi = &val; // pi 指向一个 int 型的数
int **ppi = π // ppi 指向一个 int 型的指针
graph LR
ppi-->pi-->ival,1024
解引用 int 型指针会得到一个 int 型的数,同样,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用。
#include <iostream>
using namespace std;
int main()
{
int val = 10;
int *pi = &val; // pi 指向一个 int 型的数
int **ppi = π // ppi 指向一个 int 型的指针
cout<<**ppi<<endl; // 10
}
<b style="color:red">指向指针的引用</b>
graph LR
引用-->|指向|指针
引用不是一个对象,所以不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
int i= 42;
int *p = &i;
int *&r = p; // r 是一个对指针 p 的引用
如何理解 r 的类型呢?从右向左读。先是 &,表示 r 是一个引用。声明符的其余部分确定 r 引用的类型是什么, * 说明 r 引用的是一个指针。最后,声明的基本数据类型部分指出 r 引用的是一个 int 指针。
<span style="color:red">面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。</span>
练习
// ip int 类型的指针,i int 类型的数,r int 类型的引用
int *ip, i, &r=i;
// int 类型的数 i,int 类型的指针 p,为空指针
int i, *ip=0;
// int 类型的指针 ip1, int 类型的数 ip2
int *ip1, ip2