- 0 前言
- 1 从初始化和赋值语句谈起
- 2 类型详述
- 2.1 类型详述1
- 2.2 类型详述2
- 2.3 类型详述-字面值及其类型
- 2.4 类型详述——变量及其类型
- 2.5 类型详述——(隐式)类型转换
- 3 复合类型:从指针到引用
- 3.1 复合类型:从指针到引用
- 4 常量与常量表达式类型
- 4.1 常量与变量相对,表示不可修改的对象
- 4.2 常量指针与顶层常量(top-level const)
- 4.3 常量引用(也可绑定常量)
- 4.4 常量表达式(从C++11开始)
- 5 类型别名与类型的自动推导
- 5.1 可以为类型引用别名,从而引用特殊的含义或便于使用(如:size_t)
- 5.2 两种引入类型别名的方式
- 5.3 使用using引入类型别名更好
- 5.4 类型别名与指针、引用的关系
- 5.5 类型的自动推导
- 6 域与对象的生命周期
- 7 小结
- 环境是ubuntu18.06
- IDE是clion
- 初始化/赋值语句是程序中最基本的操作,其功能是将某个值与一个对象关联起来
- 值:字面值、对象(变量或常量)所表示的值…
- 标识符:变量、常量、引用…
- 初始化基本操作
在内存中开辟空间(在栈上或在堆上),保存相应的数值
在编译器种构造符号表,将标识符与相关内存空间关联起来 - 值与对象都有类型
- 初始化/赋值可能涉及到类型转换
- 类型是一个编译器的概念,可执行文件中不存在类型的概念
- C++是强类型语言
- 引入类型是为了更好地描述程序,防止误用
- 类型描述了:
- 存储所需要的尺寸(sizeof,标准并没有严格限制)
如下:
#includeint main() { int x = 10; char y = 'a'; std::cout << sizeof(x) << 'n'; std::cout << sizeof(int) << 'n'; std::cout << sizeof(y) << 'n'; std::cout << sizeof(char) << 'n'; }
输出:
4 4 1 1
4或者1在不同的编译环境中可能有不同的结果
1 byte -> 8 bit 256
- 取值空间(std::numeric_limits,超过范围可能产生溢出)
如下:
#include#include int main() { std::cout << std::numeric_limits ::min() << std::endl; std::cout << std::numeric_limits ::max() << std::endl; unsigned int x = std::numeric_limits ::max(); x = x + 1; std::cout << x << std::endl; }
输出:
0 4294967295 0
- 对齐信息(alignof)
- CPU从内存中读取数据时,要经过一个coherency,内存中首先读取到coherency中,再到CPU中,CPU写数据也是先写道coherency中,再到内存中
- coherency在读写的时候,coherency_line为单位,coherency_line是有大小的,其可以在系统中查,在linux终端中使用cat coherency_line_size查询;如果查询的结果是64,就是说以64个字节来读写coherency,不会分开来读,如果数据被分割,就要多读一次,分开来读
- C/C++是一个性能方面的语言,从各个方面提升数据的性能,怎么样放置这样的数据,这也是一个值得考量的地方,放置不好,数据传输过程中比较慢,定义一个类型,基本就是定义一个类型的对齐信息
- int占4个字节,那么它的对齐信息就是4
- 查询相应数据的对其信息如下,且因为有对齐信息,系统有时候和想的会不一样
#include#include //8000-8007 所以8个字节 struct str { //8000-8000 1个字节 char b; //8004-8007 4个字节 int x; }; int main() { //8000-8003 //8000-8064 //假设数据存储在7999-8002 int x = 10; //首先读取8000-8064 //发现数据补全,读取8000-64 - 7999 //查询int类型的对齐信息 std::cout << alignof(int) << std::endl; //查询结构体的大小 std::cout << sizeof(str) << std::endl; }
输出:
4 8
- 可以执行的操作
基本可以进行±*/
- 类型可以划分为基本类型与复杂类型
- 基本(内建)类型:C++语言中所支持的类型
- 数值类型
1.1 字符类型(char、wchar_t、char16_t、char32_t)
1.2 整数类型
1.2.1 带符号整数类型:short,int,long,long long;其不同的环境下大小不同
1.2.2 无符号整数类型:unsigned+带符号整数类型(后面可以省略,默认为int)
1.3 浮点类型
float、double、long double
2. void
-
复杂类型:由基本类型组合、变种所产生的类型,可能是标准库引入(如vector),或自定义类型
-
与类型相关的标准未定义部分
- char是否有符号
在不同的系统中有不同的结果
可以使用unsigned char和signed char指定 - 整数中内存中的保存方式:大端(big Endian) 小端(Little Endian)
- 每种类型的大小(间接影响取值范围)
2.3 类型详述-字面值及其类型灵活变动:好处是提高了硬件的处理能力;坏处是影响程序的可移植性
C++11中引入了固定尺寸的整数模型,如int32_t
- 字面值:在程序中直接表示为一个具体数值或字符串数值
- 每个字面值都有其类型
- 整数字面值:20(十进制);024(八进制);0x14/0x14(十六进制)–int型
- 浮点数:1.3;1e8 --double型
- 字符字面值:‘c’;‘n’;‘x4d’ --char型
- 字符串字面值:”Hello“ --char[6]型
这里之所以是6个字符,是因为C语言隐式的规定,如果写一个字符串的话,会在后面隐式的加一个‘ ’,用来表示字符串的结束
- 布尔字面值:true,false --bool型
true、false一定是小写的
- 指针字面值:nullptr --nullptr_t型
- 可以为字面值引入前缀或后缀以改变其类型
- 1.3(double) – 1.3f(float)
- 2(int) --2ULL(unsigned long long)
- 可以引入自定义后缀来修改字面值类型
通常不会这样用,但要知道
如下:
#includeint operator "" _ddd(long double x) { return (int)x * 2; } int main() { int x = 3.14_ddd; std::cout << x << 'n'; }
输出为:
62.4 类型详述——变量及其类型
- 变量:对应了一段存储空间,可以改变其中内容
- 变量的类型在其首次声明(定义)时指定:
- int x :定义了一个变量x,其类型为int
- 变量声明与定义的区别:extern前缀
如下:
main.cpp下:
#include//引入外部的定义 extern int g_x;//如果在这里赋值操作,那么就会从引用变成定义,从而报错重复定义 int main() { std::cout << g_x << std::endl; }
source.cpp下:
int g_x;
输出为:
0
- 变量的初始化与赋值
- 初始化:在构造变量之处为其赋予的初始值
1.1 缺省初始化
局部变量初始值为任意值
全局变量或线程相关的变量初始值为0
对于指针也是一样
1.2 直接/拷贝初始化
拷贝初始化int x=10;
直接初始化int x(10);和int x{10};
两者有区别,但在这里没有过多的说
1.3 其他初始化
- 赋值:修改变量所保存的数值
- 为变量赋值时可能涉及到类型转换
- bool与整数之间的转换
- 浮点数与整数之间的类型转换
- 隐式类型转换不只发生在赋值时
-
if判断
-
数值比较
2.1 无符号数据与带符号数据之间的比较
C++在有符号数和无符号数之间进行默认类型转换的时候,一定是有符号数转换为无符号数
如下:
#includeint main() { int x = -1; unsigned int y = 3; std::cout << (x < y) << std::endl; }
输出为哦;
0
2.2 std::cmp_XXX (C++20)
- 可以避免2.1中的问题
- 使用该函数可以进行不同数据类型的数据之间的比较,其结果是符号数学上的定义的
- 指针:一种间接类型
- 特点
- 可以“指向”不同的对象
- 具有相同的尺寸
大部分都是8,变量的地址是一个数值,最大有多少,现在的机器都是64位机,也就是CPU总线长度是64个字节,一次性可以处理64个字节,内存最大就是2的64次方;那么保存地址的信息,在64位机就需要64个bit来保存地址信息;无论地址是指向int还是char,内部保存的都是地址信息,都需要64个bit,一个字节8个bit,所以为8个字节;若32位机,那么就是4
- 相关操作
- & - 取地址操作符
通常会把取地址操作赋予一个指针,指针可以保留相应的地址
取地址操作的对象必须是有地址的,不可&42
- * - 解引用操作符
如果是缺省初始化,对于局部指针解引用,会是随机的,对全局指针解引用,是一个0地址,0地址非常特殊,不能对其进行解引用
声明指针的时候往往要赋予一个初始值
可以直接int *p = 0;或者int *p = NULL;或者int *p = nullptr
- 指针的定义
- int *p = &val;
- int *p = nullptr;
- 关于nullptr
- 一个特殊的对象(类型为nullptr_t),表示空指针
nullptr是一个指针,其可以隐式的转换为任何一个指针
- 类似于C中的NULL,但更加安全
- 指针与bool的隐式转换:非空指针可以转换为true;空指针可以转换为false
- 指针的只要操作:解引用;增加、减少;判段是否相等 等
如下:
#includeint main() { int x = 42; int * p = &x; std::cout << p << std::endl; p = p + 1; std::cout << p << std::endl; p = p - 2; std::cout << p << std::endl; }
输出为:
00BEFB54 00BEFB58 00BEFB50
如果两个指针指向的不是数组的东西,那么不能对两个指针做><的判断,这种是非常危险的,虽然虽然运行,因为每次运行分配的地址都是不确定的,对于大小判断是没有道理的,这是指在堆里声明的指针
但是如果按照下面的程序书写,那么指针的大小就是稳定的,因为是在栈里分配的,有一个先后顺序的过程,后声明的地址大小永远小于先声明的地址大小:
#includeint main() { int x = 42; int * q = &x; int y; int* r = &y; std::cout << (r > q) << 'n'; }
输出为:
0
- void* 指针
- 没有记录对象的尺寸信息,可以保存任意地址
在极特殊情况下,我们可能会定义一个接口,这些接口不需要关注指针的具体类型,只需要知道它是一个指针就可以了
void* 是一种特殊的指针,其可以转换为任意类型的指针,也可以由任意类型的指针转换为void*
其中内部就已经丧失了很多的信息,可能会作为一个占位符,保存指针的信息
指针一定要进行初始化
如下:
#includevoid fun(void * param) {} int main() { int* r = nullptr; char* k = nullptr; fun(r); fun(k); }
- 支持判等操作,不可以对其进行加减
#includevoid fun(void * param) { //下述语句会报错 //std::cout << (param + 1) << std::endl; } int main() { int* r = nullptr; char* k = nullptr; fun(r); fun(k); std::cout << r << std::endl; std::cout << r + 1 << std::endl; fun(r); }
-
指针的指针
-
指针 VS 对象
- 指针复制成本低,读写成本高
指针是对对象的间接引用,间接引用可以减少程序的传输所付出的成本,特别自定义的对象大小是非常打的,基本上复制的话成本非常高
引用的复制成本低,但是读写成本高
- 指针的问题
- 可以为空
- 地址信息可能非法
- 解决方案:引用
- 引用
- int &ref = val;
#includeint main() { int x = 3; int& ref = x; std::cout << "x = " << x << "; ref = " << ref << std::endl; int* ptr = &x; std::cout << "*ptr = "<<*ptr << std::endl; }
输出为:
x = 3; ref = 3 *ptr = 3
- 引用是对象的别名,不能绑定字面值
可以交叉使用对象和其引用,对引用修改等价于对本体修改
并且其不可以绑定到数字这种字面值
如下:
#includeint main() { int x = 3; int& ref = x; std::cout << ref << std::endl; ref = ref + 1; std::cout << x << 'n'; }
输出为:
3 4
- 构造时绑定对象,在其生命周期内不能绑定其他对象(赋值操作会改变对象内容)
如下:
#includeint main() { int x = 3; int& ref = x; int* ptr = &x; int y = 0; *ptr = y;//改变了ptr所指向对象的内容值 ptr = &y;//改变了ptr本身的内容 int z = 1; ref = z;//相当于把z的值赋予了x //不可以做到再把ref绑定到非x //引用只有在初始化/构造的时候绑定到一个初始对象 //如果初始化引用的时候就没有把其绑定,那么之后也不能对其进行绑定,所以定义引用未初始化赋值会报错 }
- 不存在空引用,但可能存在非法引用——总的来说比指针安全
下面这个例程是非法引用的例程:
#includeint& fun() { int x; return x; //相当于 } int main() { int& ref = fun();//由于变量生命周期的存在, //所以这个引用相当于指向了一个已经销毁的对象 //所以说这是一个非法引用,虽然不会报错 }
- 属于编译器概念,在底层还是通过指针实现
- 指针的引用
- 指针是对象,因此可以定义引用,引用的引用是不存在的
对指针定义引用如下:
#includeint main() { int x = 3; int* ptr = &x; int*& ref = ptr;//定义了一个指针的引用 std::cout << *ref << std::endl;//为3 }
- int *p = &val; int * ref = p;
- 类型信息从左向右解析
- 使用const声明常量对象,const放在int前后都可以
- 是编译器概念,编译器利用其
- 防止非法操作
- 优化程序逻辑
- const int* p;和int const * p;//顶层常量/指针常量,指针指向的是常量,不能改变其值
- int* const p;//底层指针/常量指针,指向的地址不能修改
- const int* const p;
- 常量指针可指向变量
#include4.3 常量引用(也可绑定常量)int main() { int x = 4; &x; //int* --> const int* const int* ptr = &x; //const int* -不能->const int* //const int* --> const int* const int z = 3; const int* ptr_z = &z; }
- const int&
//常量引用的基本用法如下:
#includeint main() { //相当于增加了限制 int x = 3; const int& ref = x; //下面相当于放松了限制,会报错 }
- 可读不可写
- 主要用于函数形参
- 可以绑定字面值
#include4.4 常量表达式(从C++11开始)void fun(const int& param) { } int main() { //引用绑定到3的值 int x = 3; int& refx = x; //引用不可以直接绑定到字面值,下面这个是报错的 //但是常量引用可以直接绑定到字面值,下面这个是合法的 int z = 3; const int& refz = 3; //之所以要这样,因为常量引用常作为函数形参,要保证下面的可以 fun(x); fun(refx); fun(3);//相当于把fun中的形参param与3绑定起来 }
- 常量可以作为接受输入
#includeint main() { int x; std::cin >> x; //可以运行 const int y1 = x; const int y2 = 3; //但y1和y2有本质的区别 //y1的值在运行期确定 //y2的值编译器确定 if (y1 == 3) { //在编译的时候编译器会将其编译为汇编语言,等待输入然后判断是否执行 } if (y2 == 3) { //在编译期就决定了是否要执行,然后编译器就可以判断是否引入优化,提高性能 } }
- 使用constexpr声明
- 声明的是编译器常量
#includeint main() { int x; std::cin >> x; //constexpr int y1 = x;//此式报错,因为这个值只可以在运行期确定 const int y1 = x; //const int y2 = 3;//这个还有有一些办法可以修改其值的 constexpr int y2 = 3;//区别在于此式更明确的向编译器表示这就是一个编译器常量为3 //编译器可以踏踏实实的用来进行后续的优化 //使用constexpr修饰后,就完完全全的不能修改了 if (y1 == 3) { //在编译的时候编译器会将其编译为汇编语言,等待输入然后判断是否执行 } if (y2 == 3) { } }
- 编译器可以利用其进行优化
#includeint main() { constexpr int y2 = 3; //y2的类型不是constexpr int // constexpr更像是一个限定符,不是类型符 // y2 的类型还是 const int,但不仅仅是这个类型,还在编译器就确定了 }
- 常量表达式指针:constexpr位于* 左侧,但表示指针是常量表达式
注意constexpr不是一个类型符,其更像是一个限定符
#include5 类型别名与类型的自动推导 5.1 可以为类型引用别名,从而引用特殊的含义或便于使用(如:size_t)#include int main() { constexpr int y2 = 3; //表示ptr是一个常量 //ptr的类型是 const int* const constexpr const int* ptr = nullptr; //判断两个类型是否相同,此处相同返回1 std::cout << std::is_same_v << std::endl; constexpr const char* ptr2 = "123"; }
size_t是一个无符号整型,可以表示任意尺寸的对象
要根据具体的硬件环境确定size_t如何实现
硬件环境32位机,数据最大尺寸就是2的32次方,超过2的32次方没法表示,32位机上一个int是4个字节,可以用unsigned int表示任意长度的对象;这个时候就会尝试把size_t定义为unsigned int
如果是64位机,表示任意长度的对象,假设在该系统中int还是4位的,long是8位的,则可以把unsigned long定义为size_t
针对不同的系统,size_t是不同系统的别名
5.2 两种引入类型别名的方式使用size_t代码是可以在不同系统里移植的
- typedef int MyInt;
#includetypedef int MyInt; int main() { MyInt x = 3; }
- using MyInt = int;(从C++11开始)
- typedef char MyCharArr[4];
- using MyCharArr = char[4];
- 应将指针类型别名视为一个整体,在次基础上引入常量表示指针为常量的类型
#includeusing IntPtr = int*; int main() { int x = 3; //int* const ptr = &x; const IntPtr ptr = &x; *ptr = 4; }
- 不能通过类型别名构造引用的引用
C++不允许通过类型别名构造引用的引用
#include#include using RefInt = int&; using RefRefInt = RefInt&; int main() { std::cout << std::is_same_v << std::endl; }
输出位:
15.5 类型的自动推导
- 从C++11开始,可以通过初始化表达式自动推导对象类型
使用auto,必须是变量表达式
也就是auto x;
- 自动推导类型并不意味着弱化类型,对象还是强类型
python就是弱类型,变量不需要指定类型
- 自动推导的 几种常见形式
- auto:最常用的形式,但会产生类型退化
演示如下:
#include#include int main() { int x1 = 3; int& ref = x1; //int& -> int int y = ref;//ref作为右值,就会从int&类型退化为int类型 //auto会产生类型退化 //int& -> int auto ref2 = ref;//此时ref2的类型是int std::cout << std::is_same_v << std::endl; }
输出位:
0
- const auto/constexpr auto:推导出的是常量/常量表达式类型
#include#include int main() { const auto x = 3; std::cout << std::is_same_v << std::endl; constexpr auto y = 3; std::cout << std::is_same_v << std::endl; }
输出:
1 1
- auto&:推导出引用类型,避免类型退化
#include#include int main() { const auto& z = 3; std::cout << std::is_same_v << std::endl; const int x1 = 3; auto y1 = x1; std::cout << std::is_same_v << std::endl; const int x2 = 3; auto& y2 = x2; std::cout << std::is_same_v << std::endl; const int& x3 = 3; auto& y3 = x3; std::cout << std::is_same_v << std::endl; }
输出:
1 1 1 1
自动推导数组如下:
#include#include int main() { //数组的类型自动推导 int x4[3] = { 1,2,3 }; auto x5 = x4; std::cout << std::is_same_v << std::endl; //数组的类型自动推导 int x6[3] = { 1,2,3 }; auto& x7 = x6; std::cout << std::is_same_v << std::endl; }
输出:
1 1
使用auto&的缺点,为什么使用decltype
#include#include int main() { int x = 3; const int y1 = x; //为了使用aoto避免类型退化使用&,不是百分百可靠 auto& y2 = y1; decltype(y1) y3 = y1; //这里auto使用auto&,反而将其多赋予了引用的类型 std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; }
输出
0 1
- decltype(exp):返回exp表达式的类型(左值加引用)
#include#include int main() { int x = 3; int* ptr = &x; std::cout << std::is_same_v << std::endl; }
输出为:
1
- decltype(val):返回val的类型
当然,如果左值是一个变量名称,不用加&
#include#include int main() { int x = 3; int* ptr = &x; (x) = 5; const int y1 = 3; const int& y2 = y1; std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; //x是一个变量名称;(x)是一个表达式 std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; std::cout << std::is_same_v << std::endl; //不存在引用的引用,所以还是引用 std::cout << std::is_same_v << std::endl; }
输出:
1 1 1 1 1 1 1 1 1
- decltype(auto):从C++14开始支持,简化decltype使用
#include#include int main() { //这种写法特别啰嗦 decltype(3.5 + 15l) x = 3.5 + 15l; //简便写法,等价于上面 decltype(auto) x = 3.5 + 15l; }
- concept auto:从C++20开始支持,表示一系列类型(std:integral auto x =3;)
- 域(scope)表示了程序中的一部分,其中的名称有唯一的含义
- 全局域(global scope):程序最外围的域,其中定义的是全局对象
- 块域(block scope),使用大括号所限定的域,其中定义的是局部变量
- 还存在其他的域:类域,名字空间域…
- 域可以嵌套,嵌套域中定义的名称可以隐藏外部域中定义的名称
- 对象的生命周期起始于被初始化的时刻,终止于被销毁的时刻
- 通常来说
- 全局对象的生命周期是整个程序的运行期间
- 局部对象的生命周期起源于对象的初始化位置,终止于所在域被执行完成



