C++语法笔记
Author:黄金发の魔理沙
前言
C++语法备忘
C++语法——数据抽象与类型系统
C++类型系统
某个变量的引用等价于这个变量,相当于一个别名,通过引用可以直接操作原变量。引用类型的变量必须在定义时初始化,且不能再引用其他变量。
1 | int n = 4; |
C语言的函数参数传递均为值传递,而C++中的引用类型作为函数参数可以提供引用传递机制,这样可以直接修改实参的值。
1 | void swap(int &a, int &b) { |
函数的返回值可以是引用
1 | int n = 14; |
C++类型推导
C++11引入了auto关键字,用于自动推导变量的类型。auto关键字可以自动推导出变量的类型,但是不能用于数组,函数参数(C++20后允许)、类的非静态成员变量、类的非静态成员函数等场景。
1 | auto i = 100; // i 是 int |
统一初始化
C++11的统一初始化:所有数据类型允许使用花括号{}来初始化。
会进行严格的类型检查,防止窄化转换导致丢失数据精度。
1 | int n{5}; |
常量与只读变量
const修饰类型说明符,在运行过程中该变量的值不能改变。
1 | const int d; // error |
const修饰引用类型:只读引用。不能通过只读引用去修改引用的内容(可以用别的办法修改)
1 | int n = 10; |
TODO:const与指针、引用
动态内存分配
new,delete
1 | int *p = new int; |
过程抽象与函数
缺省参数
C++允许函数的参数有默认值,这样在调用函数时可以省略有默认值的参数。
1 | void func(int a, int b = 10, int c = 20){ |
函数重载
同一作用域内名字相同,形参列表不同的多个函数
内联函数
将整个函数代码插入到调用语句处,节省函数调用和返回的开销
C++的inline关键字建议编译器实现内联函数处理。使用方法:在函数定义前加inline关键字
函数指针
定义:
1 | int (*ptr)(int, int) = add; // 括号不能省略 |
使用方法:
1 | int add(int a, int b){ |
函数指针也可作为形参。
Lambda表达式(TODO)
只调用一次的简单匿名函数
声明方式:
1 | //[捕获列表](参数列表) -> 返回值类型 {函数体} |
C++的面向对象
C++的类,对象,成员
1 | // 类的定义 |
成员函数既可以在类内部实现,也可以在类外部实现(这是通常的做法)。
成员函数(又称为方法)的内部实现形式为:
1 | //返回值类型 类名::函数名(参数列表){/*实现*/} |
这种形式编译时在声明处自动展开(同时会使编译后代码变长,所以只适用于非常简短的函数),运行时不必付出函数调用的代价。
成员函数(又称为方法)的外部实现形式为:
1 | //返回值类型 类名::函数名(参数列表){/*实现*/} |
::前面的标识符就是查找范围。如果前面没有标识符,表示查找范围为全局范围。这种形式更常见,好处是可以通过使用头文件使得源代码的组织形式更为清晰。
调用一个类对象的成员函数相当于OOP编程语言的“向对象发送一条信息”这个术语。此外,每个成员函数都有一个this指针参数(并未显式出现),允许对象在成员函数内部引用对象本身。
1 | class Fruit{ |
struct和class定义类的区别:struct默认公有成员,class默认private成员
对象的生命周期
声明和定义→内存分配→初始化→使用→销毁
构造函数与析构函数
可以通过重载定义多个构造函数
一个特殊的构造函数:复制构造函数。只有一个参数,这个参数史对同类对象的引用:
形如 X::X( X& ) 或 X::X( const X& ) 二者选一,后者能以常量对象作为参数。如果不定义复制构造函数的话编译器会默认生成一个.
1 | class Complex { |
绝大多数类至少具有一个构造函数。当创建类的一个对象时,会隐式地调用构造函数,负责对象的初始化。相对应地,类也存在一个清理函数,称为析构函数。
1 | class Fruit{ |
当创建类的一个对象时,会自动调用构造函数,程序员永远不应该显示调用构造函数。至于全局和静态对象,程序开始时会自动调用它们的构造函数,而当程序终止时,会自动调用它们的析构函数。构造函数和析构函数都违反了C语言中“一切自己负责”,“语言的任何部分都不应该通过隐藏的运行时程序来实现”的设计哲学。
继承——复用已定义的操作
继承类似于科学分类法,每一层新分类都是对上一层的细化,比如从水果类派生出苹果类,同时多一些自身独有的操作(成员函数)
注意:继承类与嵌套类(类中类)不同,继承表示一个对象时另一个更为普遍的父对象的特型:我们不会认为哺乳动物内嵌套了一条狗,而是会认为狗继承了哺乳动物的特征。
C++实现:
1
2
3
4
5
6
7
8
9
10
11class Fruit{
public:void peel();
private:int weight,calories_per_oz;
};
//派生类:
class Apple:public Fruit{
public:
void make_candy_apple(float weight);
};
//派生类对象的声明:
Apple teachers;嵌套通常用于实现容器类(栈,队列等)。现在C++增加了模板这个特性,也用于实现容器类
重载——作用于不同类型的同一操作具有相同的名字
- 重载就是简单地复用一个现存的名字,但它操作的是一个不同的类型。它可以是函数的名字,也可以是一个操作符
- 尽量为相似的操作进行操作符重载,不要做一些反人类的操作,例如把乘法重载为除法
C++操作符重载实现
首先,增加水果类的加法操作符原型
1 | class Fruit{ |
&f表示它是通过传址调用的。重载在C++的I/O中也非常方便,详见后述。
C++函数重载的实现
- 同名函数,不同类型形参:调用时根据传入实参类型找对应函数
- 同名函数,不匹配的参数类型:编译器会做出判断,把实参隐式转换为匹配的形参类型,再调用该函数
- 如果上述两种方法都不适用,即编译器无法确定哪个唯一的函数该被调用,则无法通过编译
多态——运行时绑定
多态在C++中的意思是支持相关的对象具有不同的成员函数(但原型相同),并允许对象与适当的成员函数进行运行时绑定。
这句话看不懂没关系,看第二种解释:多态是指一个函数/操作符只有一个名字,却能用于几个不同的派生类型。每个对象都实现这个操作的一种变型,表现一种最适合自身的行为
对同一个操作复用,但是稍加改变,这样不同的对象都能用这个操作
还不懂?没事,看代码:
1
2
3
4
5
6
7class Fruit{
public:void peel();
slice();
juice();
private:int weight,calories_per_oz;
};
void Fruit::peel(){printf("fruit in peel");}现在声明一个水果对象,并调用peel()成员函数:
1
2Fruit banana;
banana.peel();将得到一条信息。现在从水果类派生出苹果类,并实现苹果类自己的peel()自己的成员函数:
1
2
3
4
5class Apple:public Fruit{
public:void peel(); //可以与基类重名,因为C++会覆盖处理
private:int weight,calories_per_oz;
};
void Fruit::peel(){printf("apple in peel");}这时候声明一个指向水果类的指针,并让它指向一个苹果对象(它继承于苹果类):
1
2Fruit *p=new Apple;
p->peel();打印结果是fruit in peel!看来真正被调用的是基类的peel成员函数。这是因为,如果想使用派生类的成员函数取代基类的成员函数时,C++要求你必须预先通知编译器,也就是在可能会被取代的成员函数前加上virtual关键字,意思为“不让用户看到事实上存在的东西(基类的成员函数)”。
所谓虚函数,就是在基类中声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义这个函数。程序运行时,使用指针指向某一派生类对象,这样就能调用指针所指的该派生类对象中的函数,而不会调用其他派生类中的函数。
此外,无论怎样,如果想调用基类的成员函数,可以使用这个调用方法:1
p->Fruit::peel();
既然并不是每个成员函数调用都需要这种运行时的间接模式,就应该显式告诉编译器哪些成员函数需要多态:
1
2
3
4
5class Apple:public Fruit{
public:virtual void peel(); //加一个virtual
private:int weight,calories_per_oz;
};
void Fruit::peel(){printf("apple in peel");}这样再调用输出就是apple in peel了。运行时系统将查看调用虚拟函数的对象,并选择适合该对象的成员函数,如果是一个派生类对象,调用时就不会调用基类版本的成员函数,而是调用派生类的成员函数。
从C到C++
头文件
软院算法无脑用C++万能头文件就行了
1 |
|
命名空间
C++ 的命名空间机制可以用来解决复杂项目中名字冲突的问题。
参见命名空间 - OI Wiki
算法上机等单个程序,建议使用using namespace std;,这样就不用每次都写std::了
C++输入输出流
C++的输入输出是通过流对象实现的,标准流对象cin,cout和流运算符等信息存放在头文件iostream中
“>>”称为流提取运算符,
“<<”称为流插入运算符。
使用操作符而不是函数来操作IO有以下优点:
- 可以为任何类型定义操作符。这意味着不需要scanf的%d
- 输出信息时,可以用操作符把多个IO操作数联系在一起,如:
1
cout << "THE VALUE IS"<< i << endl;
- 简化了scanf这样的函数的格式控制和使用方法(笔者注:感觉这条和1一样)
- 对“>>”“<<”重载,在一个单一的操作读取和书写整个对象
string字符串
1 |
|
- 可以用cin,scanf等读入,都是忽略前面空白符,读到分隔符(空格,换行)停止
- 可以和C字符串一样,s[0]看首位
- 可以直接用来赋值,拼接等操作
- 但是不能直接由字符数组赋值
- STL的一些用法:
1 | string a="HelloHello"; |
C++的其他要点
异常,模板,内联(inline)函数,new和delete操作符(比malloc和free更方便)
起步和学习C++
精通C++的某个特性没什么用,大多数程序员选择只使用C+++中较简单的一个子集。这个子集包括:
- 尽量使用的C++特性:
- 类
- 构造函数和析构函数,但只限于函数体非常简单的例子
- 重载,包括操作符重载和IO
- 单重继承和多态
- 避免使用的C++特性:
- 模板
- 异常
- 虚基类
- 多重继承
C++或许过于复杂,但是是唯一可行的方案
C++确实对C有一些改进,但也保留了C的一些缺陷,同时妥协了一部分C语言的设计哲学之一“所有特性都不需要隐式的运行时支持”。
C++对C的改进:
- C++中,字符数组末尾不加\0是不合法的,C语言合法
- C++允许使用一个常量整数(const int)来定义数组大小,C语言不允许
- …
STL速成
- 省流:每个C++标准,比如C++11等都有一些标准模板库(Standard Template Library,STL)将很多有用的内容封装过了,直接就能用,包括各类容器(如队列,栈等),算法(排序等)和一些其他功能。
- 软院算法上机是C++14
容器
- 可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是类模板,但是涉及到排序的sort函数,set容器等,如果操作的是其他类型或者自定义排序方式,则需要重载比较运算符或者定义比较函数
- 对象被插入容器中时,被插入的是对象的一个复制品
容器的分类
顺序容器
vector,deque,list,forward_list,array,string
一般vector(可变长度数组)和list(双向链表)用的最多
关联容器
强调key和value对应来构建关系。
有序关联容器(元素有序排列):set(集合),map(映射,有key/value,根据key对元素排序),multiset(允许相同元素),multimap(允许key相同的元素)
无序关联容器:元素按照特定顺序(乱序)存放:unordered_set,unordered_map,unordered_multiset,unordered_multimap
容器适配器
是容器模板类的实现,没有迭代器(类似指针)了:stack,queue,priority_queue
容器声明,初始化和赋值
1 | vector<int> a{1, 2, 3};// 定义 vector 对象 |
如果涉及到排序的sort函数,set,priority_queue等,操作的是其他类型或者自定义排序方式,则需要重载<运算符或者定义比较方法(重载”()”)
1 | sort(a,a+n,greater<int>);//降序排序 greater return x>y; |
容器的操作
大部分标准库容器(指包含容器适配器)共有的成员函数
- 相当于按词典顺序比较两个容器的运算符:=, < , <= , > , >=, ==
- empty :判断容器中是否有元素
- max_size :容器中最多能装多少元素
- size :容器中元素个数
- swap :交换两个容器的内容
顺序容器和关联容器中都有的成员函数
- begin 返回指向容器中第一个元素的迭代器
- end 返回指向容器中最后一个元素后面的位置的迭代器
- rbegin 返回指向容器中最后一个元素的迭代器
- rend 返回指向容器中第一个元素前面的位置的迭代器
- erase 从容器中删除一个或几个元素
- clear 从容器中删除所有元素
注意:迭代器是左闭右开区间,所以尾部要多一个位置指向空白元素。r是反向。如果迭代器到达了容器中的最后一个元素的后面,此时再使用它,就会出错,类似于使用 nullptr 或未初始化的指针一样
顺序容器的常用成员函数
- front : 返回容器中第一个元素的引用
- back : 返回容器中最后一个元素的引用
- push_back : 在容器末尾增加新元素
- pop_back : 删除容器末尾的元素
- erase : 删除迭代器指向的元素 可能会使该迭代器失效,或删除一个区间,返回被删除元素后面的那个元素的迭代器
关联容器的常用成员函数
- find : 返回一个迭代器,指向第一个关键字等于给定值的元素,如果没有这样的元素,则返回尾后迭代器
- insert : 向容器中插入一个元素,返回一个pair,包含一个迭代器和一个bool值,表示插入是否成功
- count : 返回给定关键字出现的次数
- remove : 删除容器中所有关键字等于给定值的元素
容器的迭代器(当成指针用)
1 | vector<int> a{1, 2, 3}; |
注意:最好不要在循环体内部增删元素否则迭代器会失效,除非调整迭代器指向的位置
算法
- STL提供能在中提供能在各种容器中通用的算法,比如插入、删除、查找、排序等,大约有70种标准算法,大多数在algorithm头文件中定义
- 算法就是一个个函数模板,通过迭代器来操纵容器中的元素
- 有的算法返回一个迭代器
- 算法可以处理容器,也可以处理普通数组,例如sort()需要重载”<”
- 区间都为左闭右开,迭代器的最右一个位置指向空白元素
1 | __gcd(a, b) // 求两个数的最大公因数 |