Skip to content

C++

开始

main非0的返回值由系统规定,一般用来指出错误类型。

cin``cout表示标准输入输出,cerr标准错误(输出),clog一般信息(输出)。

<<表示输出运算符,左侧是一个ostream对象(比如cout,cerr都是),右侧是要打印的值。

endl称为操纵符,写入endl可以结束当前行,并将与设备关联的缓冲区的内容刷到设备中。

std表示标准命名空间,命名空间解决名字定义冲突。

控制流的判断中有一种类似这样的操作:

int val;
while(std::cin >> val) {
    ...
}

这样,实际上检测的是这个std::cin对象(>>运算符返回其左侧对象),使用istream作为条件实际是检测流的状态,整数自然可以,如果是文件结束符或是无效输入的时候,istream对象状态为无效,这使得对象的条件为假。

变量和基本类型

  • bool的最小大小未定义
  • char是8位,wchar_t与char16_t是16位,char32_t是32位,后两位服务于unicode
  • short与int都是16位

类型转换方面

  • 非布尔算术->布尔:非0即true,条件判断中常出现的自动类型转换
  • 布尔->非布尔类型: false为0,true为1
  • 浮点数->整数:截断
  • 整数->浮点数:小数部分记为0,如果该整数所占空间超过了浮点类型的容量,精度将存在损失

只要出现算术运算又有无符号又有有符号的情况,将自动类型转换为无符号数。

初始化:对象在创建的时候获得了一个特定的值,我们说这个对象被初始化了。赋值则是将原有的值擦除,重新写入。

列表初始化:形如以下的方式都可以初始化一个int变量为0:

int a = 0;
int a = {0};
int a{0};
int a(0);
  • 如果初始值存在丢失信息的风险,则编译器将报错:

    cpp long double ld = 3.1415926; int a{ld}, b = {ld}; // long double给int有丢失信息的风险 int c(ld), d = ld; // 无报错

默认初始化:定义变量的时候不指定初值。

  • 定义于任何函数体之外的内置类型的变量被初始化为0
  • 定义在函数体内部的内置类型的变量不被初始化,则值为未定义的
  • 类的对象如果没有显式初始化,则其值由类决定

C++支持分离式编译机制,允许将程序分割为若干个文件,每个文件可被独立编译。

声明使得名字为程序所知,定义则负责创建于名字关联的实体。

extern关键字声明但不定义一个变量,这需要你不显式地初始化一个变量。

extern int a; // 声明但不定义
int a; // 声明并定义
extern int a = 12; // 定义

在函数体内部试图初始化一个由extern关键字标记的变量,将引发错误:

#include<iostream>

extern int a;

int main(){
    a = 12; // error!
}

作用域:大部分作用域以花括号分隔。

全局作用域:所有花括号之外;然后其他的变量作用域为其所在的最小的花括号。

作用域允许嵌套,外层的变量在内层可用,也可以在内层重新定义外层的变量。

#include<iostream>

int i = 12;

int main() {
    int j = 100;
    int i = j;
    std::cout << i << std::endl; // 100
}

引用即为对象起了另一个名字,如:

int ival = 1024;
int &refval = ival; // 正确,refval指向ival(是ival另一个名字)
int &refval2; // error! 引用类型需被初始化

定义引用时,程序把引用和它的初始值绑定在一起,而非将初始值拷贝给引用。引用即别名

指针,上过一遍了:

int val = 2;
int *pval = &val; // pval即为val的指针,即pval存放val的地址
int *pval = val; // error!

解引用符*用于输出指针存放的地址所对应的值。

空指针有三种初始化方式:

int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;

不能将int变量直接赋值给指针(这不是相当于指定地址吗),即使是0也不行。

int zero = 0;
pi = zero; // error
// another test
pi = 0; // success

void*指针是一种特殊的指针类型,存有任意对象的地址。

指针的前面那个*只作用于一个变量,即:


int main(){
    int *p, i; // p is a pointer to an integer, i is an integer variable
}

const表示该变量的值不能被改变,因此必须初始化,且对其任何的赋值操作都会报错。

默认状态下,const变量仅在当前文件中生效。

不论是声明还是定义都对const变量追加extern关键字即可共享。

const引用不能修改:

int main() {
    const int i = 0;
    const int &ri = i;
    ri = 10; // error: const引用不能修改
    int &ri2 = i; // error: i是const,ri2不是const引用报错
}

可以将const引用给非const变量:

int i = 42;
const int &ri = i; // const引用绑定到非const变量
const int &ri2 = 200;
// ri = 10; // 错误:不能修改const引用绑定的值
int &ri3 = ri * 2; // 错误:不能将const引用绑定到非const引用
std::cout << "ri: " << ri << ", ri2: " << ri2 << ", ri3: " << ri3 << std::endl;

常量指针:先const*不能修改,先*const可以修改。修改指的是指向的值。

就这么理解:从右往左读,const先出现代表这个"变量"的地址是确定的,不然就代表先确定的是这个值是个指针。

int i = 10;
int * const ptr = &i; 
*ptr = 12; // right
int const * constPtr = &i; 
*constPtr = 115415; //error
  • 顶层const表示这个指针本身是一个常量,对任何数据类型实用
  • 底层const表示指针所指的对象是一个常量,对指针引用等生效
    • 执行对象的拷贝操作时,二者需具有相同的底层const资格

constexpr:

constexpr int a = 1; // right
constexpr int b = a + 1; // right
constexpr int c = getsize(); // 只有当getsize()是一个constexpr函数时才正确

别名:

typedef double wages; // wages是double的别名
typedef wages wages1, *wages2; // wages1是double的别名,wages2是double*的别名

auto自动让编译器判断类型。

decltype比较复杂,是希望从表达式推断类型,但是不希望赋值:

decltype(f()) sum = x; // sum的类型是函数返回值的类型
const int a = 2, &c = a;
decltype(a) b = 3; // b的类型是const int
decltype(c) d = b; // d的类型是const int&,即d绑定b

有点费解的地方:

int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // b的类型是int,因为r是int&,r+0是int
decltype(*p) c; // c的类型是int&,因为*p是int&,c也是int&,因此报错:c未初始化

字符串,向量和数组

using声明,相当于省去了std::

#include<iostream>
using std::cout;

int main() {
    int a ;
    std::cin >> a; // right
    cin >> a; // wrong 
    std::cout << a << std::endl; // right
    cout << a << std::endl; // right
}

头文件不应该包含using声明。

string对象

string s1;
string s2 = "hello world";
string s3("hello world");
string s4(2, 'c'); // cc
string s1(s2); // s1是s2的副本
string s1 = s2; // s1是s2的副本

拷贝初始化(指用等号连接,s2 = s1的操作),直接初始化(指s2(s1)的操作)。

getline()函数读取一整行,包括换行符,返回值是istream对象。

string line;
while(getline(cin, line)) 
    cout << line << endl;

string类有一个叫size_type的类型,用于存储string.size(),其本质是一个无符号整数类型。

现在才知道string a = "hello" + "world"是错的,python写多了是这样的。

auto( declareation : expression )

如下操作:

int main(){
    string s = "hello";
    for (auto c : s) {
        c = toupper(c);
    }
    cout << s << endl; // hello
    for (auto &c : s) {
        c = toupper(c);
    }
    cout << s << endl; // HELLO
}

解引用就会修改每个字符了。

vector

vector是一个类模版。

如何理解模版?就是可以适应很多类型的东西。

使用是类似:

vector<int> ivec; // ivec存储int类型的元素
vector<Sales_item> sales; // sales存储Sales_item类型的元素
vector<vector<int>> file;

列表初始化:

vector<string> svec = {"hello", "world"};
// 等价于
vector<string> svec{"hello", "world"};

值初始化:

vector<int> ivec(10, -1); // ivec有10个元素,每个元素都是-1
vector<string> svec(10); // svec有10个元素,每个元素都是空字符串
vector<int> ivec(10); // ivec有10个元素,每个元素都是0

省流一下,列表初始化是依次赋值,值初始化是整体赋值。

vector使用push_back在尾部依次添加元素。

其余一些函数:

  • empty()判断vector是否为空
  • size()返回所含元素的水
  • v[n]返回第n个元素的引用
  • == / !=严格相等才是==,否则!=

vector不能以下标形式添加,只能push_back

迭代器

  • *iter 所指元素的引用
  • iter->mem 等价于 (*iter).mem
  • ++iter / --iter 上下元素
  • iter1 == iter2 / iter1 != iter2 指示的是同一个元素
string a("hello world");
if (a.begin() != a.end()) { // 检查了是否不为空
    auto it = a.begin();
    *it = toupper(*it);
}

注意,迭代器end()并不代表某个元素,所以不能对其++或解引用等操作。

for(auto it = a.begin(); it != a.end(); ++it) {
    *it = toupper(*it);
}

这样子可以将整个字符串变为大写的。

注意:push_back这种方法会破坏迭代器。

虽然说迭代器本质不是整数的index这种类型,但是可以进行数据运算(估计是重写了?):

auto it = a.begin() + 3;
auto mid_it = (a.begin() + a.end()) / 2;

当然这些的返回类型都是迭代器。

数组 array

复合类型,声明形式如a[n]

数组的初始化会自动进行,编译器也会自动进行维度的判断,如果维度没有显式声明。

int a[] = {0, 1, 2, 3, 4, 5}; // 维度为6

数组是不允许拷贝与赋值的,即以下操作不被允许:

int a[] = {0, 1, 2, 3, 4, 5};
int b[] = a; // error
b = a; // error

理解复杂的数组声明:

int *ptrs[10]; // 声明一个含有10个整型指针的数组
int &refs[10]; // error: 引用不是对象,不能是数组
int (*parray)[10] = &arr; // 声明一个指向含有10个整数的数组的指针
int (&arrRef)[10] = arr; // 声明一个引用,引用一个含有10个整数的数组

采用的方法就是从内至外,比方说有括号的都是先声明引用/指针才出现数组。

一些情况下,数组的操作实际上是指针的操作,这其中的一层意思是,当数组作为一个auto变量的初始值时,推断得到的类型是指针而不是数组:

int a[] = {0, 1, 2, 3, 4, 5};
auto a2(a); // a2是一个整型指针,指向a的第一个元素
a2 = 42; // 错误:不能给指针赋值

使用decltype关键字时这个隐式转换不会发生。

指针也是迭代器,我们模仿上面类似的操作:

int a[6] = {0, 1, 2, 3, 4, 5};
int *e = &a[6]; 
for(int *b = a; b != e; ++b) {
    cout << *b << endl;
}

使用上面的方法也还可以,但是极易出错,可以使用beginend函数:

int ia[] = {0, 1, 2, 3, 4, 5};
int *beg = begin(ia);
int *end = end(ia);

指针相减是差距的距离(因此这个类型是有符号整数),但是注意,不能对两个不相关的指针进行相减操作,如:

int a1[6] = {0, 1, 2, 3, 4, 5};
int a2[6] = {0, 1, 2, 3, 4, 5};
int *p1 = &a1[0];
int *p2 = &a2[2];
cout << p2 - p1 << endl; // error

在C语言中,比较字符数组的操作不行,而在CPP中可以,类似py(因为这里不是数组了,而是string)。

string相加的操作在C中也是不行的,因为指针相加是无意义的,C中可能得写作:

strcpy...
strcat...

多维数组

cpp,严格上来说不存在多维数组,理解为数组的数组更为妥当。

没啥好说的,有一段比较有意思:

int ia[3][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};
int (&ria)[4] = ia[1]; // ria是一个含有4个整数的数组,绑定到了ia的第二个数组

表达式

隐式的这个计算的类型转换通常是由浮点数(精度较高的)向int这类精度较低的转换。

C语言中的/是地板除,即截断小数部分。

很经典的小问题:

if(i < j < k) // 布尔值与k比较

刚刚才发现:

char cp[] = "Hello world";
cout << cp << endl;
cout << *cp << endl;

上面那个居然输出的是整个字符串,我一直以为会输出一个指针地址,ohno太神奇了。

原来是cout会重载,可以。

赋值操作会有类型转换,右侧转为左侧的类型。

赋值为右结合律,我感觉挺自然的,就该这样.jpg。

点运算符获取成员,箭头运算符则是取值后的点运算符,即ptr->mem等价于(*ptr).mem,这个括号挺重要的。

条件运算符,即...?...:...结构,是左结合的。

  • ~求反
  • &
  • |
  • ^异或

sizeof返回size_t类型,内容是表达式结果类型的大小。

逗号运算符,规定了一个运算对象求值的顺序。从左向右。

类型转换

  • 大多数表达式中,比int类型小的整数类型首先提升为较大的整数类型
  • 在条件中,非布尔值转换为布尔值
  • 初始化过程中,初始值转换成变量的类型;赋值语句中,右侧转换为左侧的类型
  • 运算的对象有很多类型,需要转换为同一种类型
  • 函数调用也会出现类型转换
  • 大多数用到数组的表达式中,数组自动转换为指向数组首元素的指针

强制类型转换

如下形式:cast-name<type>(expression);

cast-name可以是static_castdynamic_castconst_castreinterpret_cast

  • static_cast只要不包含底层const,都可以使用
  • const_cast只能改变原酸对象的底层const