目录

对象与基本类型

第 2 章笔记


初始化与赋值

value

类型详述

类型描述的信息

#include <limits>
int main(int argc, char const *argv[])
{
    int x{0};
    std::cout << "Size of: " << sizeof(x) << std::endl; 
    std::cout << "Int min: " << std::numeric_limits<int>::min() <<std::endl;
    std::cout << "Int max: " << std::numeric_limits<int>::max() <<std::endl;
    std::cout << "Alignof int" << alignof(int) << std::endl;
    return 0;
}

类型

基本(内建)类型

只有 unsigned 指代 unsigned int

复杂类型

未定义部分

unsigned char c1;
signed char c2;

字面值

指定字面值的类型

// 指定 float 类型的字面值
float x = 1.3f;
// 指定一个 unsigned long long 类型的字面值
unsigned long long y = 2ULL;

用户自定义的字面值

C++ 11 允许用户自定义字面值的类型。该定义通过重写运算符 "" 进行定义。该功能可以方便我们定义一些单位(比如时间 / 重量)等等。下面的字面值代表了将接收到的 double 乘以 2 再转化为 int 的结果

int operator "" _customType(long double x)
{
    return (int)x * 2;
}

int main(int argc, char const *argv[])
{
    //using _customType as an literal type
    int x = 3.14_customType;
    std::cout << x << std::endl;
}
需要注意的是,User-defined literals 能接收的参数类型是有限的(引入的函数接收的类型是有限的)。

变量及其类型

变量的声明与定义

extern keyword

extern:实现的是定义只有一处,声明可以有多处的概念,用于外部文件引用实现文件中的定义。

为什么要使用 extern 关键字?

假设我们有两个文件需要一起编译。A 中使用调用 B 中已经定义的变量 x。如果没有 extern,那么会出现两种情况:

因此,如果希望在 A 中使用 B 中定义的 x,那么必须使用 extern 关键字。extern 会让编译器将定义视作外部文件变量在本地的声明,从而达到我们希望的效果:

//source.cpp
int x;

//main.cpp
extern int x;
int main()
{
    std::cout << g_x << std::endl;
}

extern 引入的变量如果进行初始化,那么就会从声明转化为定义。

变量的初始化和赋值

初始化的方式

//direct init
int x(10);
//copy init
int x = 10;
//list init
int x{10};

(隐式)类型转换

复合类型

指针

指针的定义

//p points to the address of the val
int* p = &val;

指针的操作

// 指针指向内容随机,解引用结果无法预测
int *p;
// null pointer
// 访问报错,但是内容稳定,方便查错
// 缺点是存在整型到指针的隐式转换
int *p = 0;
int *p = NULL;
// 防止隐式转换,C++ 11提供的 nullptr 指针对象
int *p = nullptr;

void* 指针
指向指针的指针

int x = 42;
int *p = &x;
int **pp = &p;

指针是对对象的间接引用:

  • 其好处在于可以减少程序传递的成本。这点在复制比较大的对象上尤其明显。其次,某些对象不可复制,因此只能通过复制其地址来达到复制的效果。
  • 但带来的缺点是读写成本高:因为读写之前都需要解引用
  • 指针可以修改指向对象的值:
    • 普通情况下,argument 传递给 parameter 是通过值(拷贝)传递,修改 parameter 的值不会影响 argument
    • 传指针时,argument 和 parameter 指向的是同样的内存地址,因此修改 parameter 也会导致 argument 的改变
指针与 bool 的隐式转换

引用

指针可能存在的问题:指向的对象为空或者非法。采用引用是另外一个较好的选择。引用是变量的别名

int x = 42;
//& is an type modifier here
int &refX = x;

int x = 0;
int y = 1;
int *ptr = &x;
//打印 ptr 结果相同,但实际上是不同的操作
//改变 x 所在内存的值为 y, 1 in x
*ptr = y;
//改变了 ptr 指向的位置,从 x 到 y,1 in y
ptr = &y;

从上面的例子来看,引用只能改变其指向内存地址中的内容,而不能改变其指向的位置。这要求引用在初始化的时候必须绑定一个实际的对象(的地址)

需要注意非法引用的问题:通过函数返回局部变量的引用可能出现问题。

指针的引用

int* &refPx = ptr;

常量与常量表达式

常量指针与顶层常量

指针与常量结合时,需要考虑哪个部分不能被修改:

如果限制的是指针本身,那么:

// 指针本身无法改变
int* const ptr = &x;
如果限制的是指针指向的内容:,那么:
//指针可以改指其他位置,不能通过该指针改变其指向的内容
const int* ptr = &x;

const 出现在 * 左边代表指针本身不能指向其他地方。

顶层常量
底层常量的隐式转换

// int * to const int *
int x = 4;
// &x 由 int* 转化为了 const int*,该指针之前可以对 x 进行读写,但现在只能读取 x
const int *ptr = &x;

// const int * to int *
const int x = 4;
// error, &x 无法写 x,因此 ptr 也不能对 x 进行写操作
// low-level const 是有传递性的,如果之前只读,那么之后也必须只读(不管通过什么样的途径,比如赋值,参数传递等等)
int* ptr = &x;
// good
const int* ptr = &x;

常量引用

常量表达式

不同的常量确定的时机不同。比如下面的例子:

int y;
std::cin >> y;
// y1 是在运行期确定的(运行期常量)
const int y1 = y;
// y2 是在编译期确定的(编译器常量)
const int y2 = 3;
由于这样的问题,编译器在处理 y1y2 时,就会有不同的方式。比如以上述的变量做条件判断 if (y1 == 3)

也就是说,编译器常量是可以被编译器优化的。这也是常量表达式的由来:C++ 11 中通过 constexpr 关键字,将常量直接声明为编译期常量。也就是说,上述的 const int y2 = 3 可以写为:

// y2 被显式的声明为编译期常量
// y2 的类型是 const int
// constexpr 是一种带指导意义的限定符(//specifier//),不属于类型声明的一部分
constexpr int y2 = 3;

常量表达式指针

指针也可以作为编译器常量。这种情况下,需要满足两个条件:

只有满足这两个条件,指针才能作为编译器常量对编译器可见。也就是说,这种情况下的指针类型是 const type* const

constexpr const int* ptr = nullptr;
//字符串
constexpr const char* str = "abc";

可以使用 std::is_same_v<type1, type2> 来判断类型是否相同。定义于 <type_traits> 头文件

类型别名与自动推导

类型别名

类型可以引入别名,便利使用(比如 size_t 实际上是一个 unsigned int)。别名的引入有两种方式:

//myInt 是 int 类型的别名
//推荐使用 using
//别名实际上是 myCharArr, 但 typedef 的引用看起来非常 confusing
typedef char myCharArr[4];
using myCharArr = char[4];

类型别名与指针和引用

using intP = int*;
int x = 3;

//别名定义
const intP constIntPAlias = &x;
//等同于 top-const pointer
int* const constIntP = &x;

类型推导

C++11 允许使用 auto 关键字让编译器自动通过初始化表达式的结果推断变量的类型:

auto x = 3.5 + 15l;

auto

int x1 = 3;
int& ref = x1;
//类型退化,ref 作为右值,此处由 int& 退化为 Int, 而不是 int&
int y = ref;
//auto 导致类型退化, ref2 是 int 类型
auto ref2 = ref;

//const int 
const auto x1 = 3;
//const int& 
const auto& x2 = 3;

const int x3 = 3;
//const int
const auto y1 = x3;
//int (top-const 的退化)
auto z = x3;

//不会退化,const int&
auto& y2 = x3;

int x[3] = {1,2,3};
//int*
auto x1 = x;
//加引用不会退化:int(&)[3] 
auto& x2 = x;

decltype

decltype 获取一个表达式,并返回表达式的类型。decltype 与 auto 的区别在于,decltype 不会产生类型退化

//x is an variable name
int x = 3;
//int
decltype(x);
//注意加括号的形式,(x) 是表达式, x 是变量名
//int&
decltype((x));

int x = 3;
int& y1 = x;
//int
auto y2 = y1;
//int&
decltype(y1) y3 = y1;

int x = 3;
int* ptr = &x;
//*ptr 是包含了解引用操作符的表达式:此处是 l-value,此处得到的是 int& 
decltype(*ptr);

//非常繁琐的写法
decltype(3.5+15l) x  = 3.5 + 15l;
//C++ 14 的写法
// 用 auto 来代表繁琐的表达式,并不会导致退化
// 编译器会将 auto 替换为赋值运算符右边的内容
decltype(autol) x  = 3.5 + 15l;

#include <concepts>
int main()
{
    //int
    std::integral auto z = 3;
    //error, 3.5 is not an integral
    std::integral auto z = 3.5;
}

域和对象生命周期

域 Scope

域代表了程序的一部分,域中的 name 有唯一的含义

域可以进行嵌套,内部域中的 Name 会掩盖外部域中的 Name:

int x = 3;
int main()
{
   int x = 4;
   //call local x
   std::cout << x << '\n';
}

对象生命周期

生命周期指对象从被初始化到被销毁的区间。