环境: CLion, C++20
注意, oj的C++环境为C++11
shift + Enter
: 下方新建一行, 光标移至该行行首
Ctrl + Alt + Enter
: 上方新建一行, 光标移至该行行首
Ctrl + Enter
: 在句首: 上方新建一行, 光标不动; 在句尾: 下方新建一行, 光标不动. 在行中间会把行截断
Ctrl + D
: 复制一行
Ctrl + Y
: 删除行. 注意, 这个快捷键也可以被设定为重做
Ctrl + W
: 增强选中. 即, 选中光标所在的那个单词. 注意, 这个不是关闭窗口
Alt + Shift + G
: 光标移至行末
Alt + Shift + Up/Dn
: 按行上移/下移(?)
Ctrl + Shift + Up/Dn
: 按代码块上移/下移该(?)
Ctrl + /
: 注释
运算符 | 描述 | 例子 | 可重载性 |
---|---|---|---|
第一级别 | |||
:: | 作用域解析符 | Class::age = 2; | 不可重载 |
第二级别 | |||
() | 函数调用 | isdigit('1') | 可重载 |
() | 成员初始化 | c_tor(int x, int y) : _x(x), _y(y*10){}; | 可重载 |
[] | 数组数据获取 | array[4] = 2; | 可重载 |
-> | 指针型成员调用 | ptr->age = 34; | 可重载 |
. | 对象型成员调用 | obj.age = 34; | 不可重载 |
++ | 后自增运算符 | for( int i = 0; i < 10; i++ ) cout | 可重载 |
-- | 后自减运算符 | for( int i = 10; i > 0; i-- ) cout | 可重载 |
const_cast | 特殊属性转换 | const_cast(type_from); | 不可重载 |
dynamic_cast | 特殊属性转换 | dynamic_cast(type_from); | 不可重载 |
static_cast | 特殊属性转换 | static_cast(type_from); | 不可重载 |
reinterpret_cast | 特殊属性转换 | reinterpret_cast(type_from); | 不可重载 |
typeid | 对象类型符 | cout << typeid(var).name();cout << typeid(type).name(); | 不可重载 |
第三级别(具有右结合性) | |||
! | 逻辑取反 | if( !done ) … | 可重载 |
not | ! 的另一种表达 | ||
~ | 按位取反 | flags = ~flags; | 可重载 |
compl | ~的另一种表达 | ||
++ | 预自增运算符 | for( i = 0; i < 10; ++i ) cout | 可重载 |
-- | 预自减运算符 | for( i = 10; i > 0; --i ) cout | 可重载 |
- | 负号 | int i = -1; | 可重载 |
+ | 正号 | int i = +1; | 可重载 |
* | 指针取值 | int data = *intPtr; | 可重载 |
& | 值取指针 | int *intPtr = &data; | 可重载 |
new | 动态元素内存分配 | long *pVar = new long;MyClass *ptr = new MyClass(args); | 可重载 |
new [] | 动态数组内存分配 | long *array = new long[n]; | 可重载 |
delete | 动态析构元素内存 | delete pVar; | 可重载 |
delete [] | 动态析构数组内存 | delete [] array; | 可重载 |
(type) | 强制类型转换 | int i = (int) floatNum; | 可重载 |
sizeof | 返回类型内存 | int size = sizeof floatNum;int size = sizeof(float); | 不可重载 |
第四级别 | |||
->* | 类指针成员引用 | ptr->*var = 24; | 可重载 |
.* | 类对象成员引用 | obj.*var = 24; | 不可重载 |
第五级别 | |||
* | 乘法 | int i = 2 * 4; | 可重载 |
/ | 除法 | float f = 10.0 / 3.0; | 可重载 |
% | 取余数(模运算) | int rem = 4 % 3; | 可重载 |
第六级别 | |||
+ | 加法 | int i = 2 + 3; | 可重载 |
- | 减法 | int i = 5 - 1; | 可重载 |
第七级别 | |||
<< |
位左移 | int flags = 33 | 可重载 |
>> |
位右移 | int flags = 33 >> 1; | 可重载 |
第八级别 | |||
< |
小于 | if( i < 42 ) … | 可重载 |
<= |
小于等于 | if( i | 可重载 |
> |
大于 | if( i > 42 ) … | 可重载 |
>= |
大于等于 | if( i >= 42 ) ... | 可重载 |
第九级别 | |||
== |
等于 | if( i == 42 ) ... | 可重载 |
eq | == 的另一种表达 | ||
!= | 不等于 | if( i != 42 ) … | 可重载 |
not_eq | !=的另一种表达 | ||
第十级别 | |||
& | 位且运算 | flags = flags & 42; | 可重载 |
bitand | &的另一种表达 | ||
第十一级别 | |||
^ | 位异或运算 | flags = flags ^ 42; | 可重载 |
xor | ^的另一种表达 | ||
第十二级别 | |||
| | 位或运算 | flags = flags | 42; | 可重载 |
bitor | |的另一种表达 | ||
第十三级别 | |||
&& | 逻辑且运算 | if( conditionA && conditionB ) … | 可重载 |
and | &&的另一种表达 | ||
第十四级别 | |||
|| | 逻辑或运算 | if( conditionA || conditionB ) ... | 可重载 |
or | ||的另一种表达 | ||
第十五级别(具有右结合性) | |||
? : | 条件运算符 | int i = (a > b) ? a : b; | 不可重载 |
第十六级别(具有右结合性) | |||
= | 赋值 | int a = b; | 可重载 |
+= | 加赋值运算 | a += 3; | 可重载 |
-= | 减赋值运算 | b -= 4; | 可重载 |
*= | 乘赋值运算 | a *= 5; | 可重载 |
/= | 除赋值运算 | a /= 2; | 可重载 |
%= | 模赋值运算 | a %= 3; | 可重载 |
&= | 位且赋值运算 | flags &= new_flags; | 可重载 |
and_eq | &= 的另一种表达 | ||
^= | 位异或赋值运算 | flags ^= new_flags; | 可重载 |
xor_eq | ^=的另一种表达 | ||
|= | 位或赋值运算 | flags |= new_flags; | 可重载 |
or_eq | |=的另一种表达 | ||
位左移赋值运算 | flags | 可重载 | |
>>= | 位右移赋值运算 | flags >>= 2; | 可重载 |
第十七级别 | |||
throw | 异常抛出 | throw EClass(“Message”); | 不可重载 |
第十八级别 | |||
, | 逗号分隔符 | for( i = 0, j = 0; i < 10; i++, j++ ) … | 可重载 |
不能用来声明变量
主要用来限定函数的返回值类型或参数类型: 如果函数无返回值/参数, 则应该声明其返回值/参数为void
类型
可以用来声明指针. 也可以把任何类型的指针传给它
如果函数的参数可以是任意类型的指针, 则应该声明参数类型为void*
C++中支持bool
类型, 只占用一个字节. true
或者不等于0
的数(整型&浮点型均可)为真, false
或0
为假. 但是在显示的时候, 只会用1
表示true
, 用0
表示false
#include<cstring>
atoi()
stoi()
32位系统下的指针(各种类型)长度均为4字节, 64位系统均为8字节
指针类型的意义在于告诉编译器处理地址的时候往后延伸几个字节(以及该按何种方式解析读出来的二进制数据)
对于某一变量, 如a
, 那么&a
便是取a
的地址
对于某一指针变量p
, 那么*p
便是该指针指向的内容
int n = 6;
int *p1; // 初始化指针变量
p1 = &n; // 把n的地址传给p1
int *p2 = &n; // 也可采用这种方式初始化
// 输出指针p1和p2所存储的地址. 可以发现它们指向同一片内存空间
cout << p1 << endl << p2 << endl;
cout << *p1 << endl; // 输出p1指针指向的内容
对于某一指针*p
, 如果使用p++
, 那么偏移量为该指针基类型的长度
类型名 (*指针变量名)(参数类型1, 参数类型2, ...){}
如: int (*pf)(int, char){}
表示pf
是一个函数指针, 它所指向的函数, 返回值类型应是int
. 该函数应有两个参数, 第一个是int
类型, 第二个是char
类型
int *p[3];// 一个数组, 其中包含3个int*类型的变量, 它们又各自指向三个int或者int[]
int i = 1;
cout << p[i] << endl;// 输出的是指针数组中的第i个元素, 它仍然是一个指针
cout << *(p + i)<< endl;// 同上
cout << **(p + i) << endl;// 这样才是输出第i个指针指向的内容
它是一个指针数组, 数组内的元素类型为int*
注意int (*p)[3]
和int *p[3]
(相当于(int *)(p[3])
)的区别!!!
数组名即为指向其中第一个元素的指针
要访问第i
个元素, 有两种方法:
int n[4] = {1, 2, 3, 4};
int i = 2;
cout << n[i] << endl;
cout << *(n + i) << endl;
123
在C++中, 能用string
就不要用char *
!!!
单引号是字符, 双引号是字符串
-
char[]
char a[] = "string1"
; 实现了2个操作:- 声明一个
char
的数组, - 为该数组“赋值”, 即将
"string1"
的每一个字符分别赋值给数组的每一个元素
- 声明一个
-
char*
char * a = "string2"
; 实现了3个操作:- 声明一个
char*
变量; - 在内存中的文字常量区中开辟了一个空间存储字符串常量
"string2"
- 返回这个区域的地址, 赋给字符指针变量
a
- 声明一个
以\0
结尾. 如sizeof("Hello");
的返回值是6
所以, 动态分配字符数组的时候, 长度记得+1
cin >>
. 这个方式读取字符串, 遇到空格或者制表符就会停止
cin.getline()
读取指定长度的字符串, 在遇到\n
或指定的字符时停止
==这里参考输入输出==
需要#include <cstring>
以及和using namespace std;
配合使用
这些是操作char*
类型的, 不是操作string
类型的. 注意char*
类型不支持+
连接字符串, =
给字符串赋值等运算符重载. 这些是string
的特性
函数 | 功能 |
---|---|
strcpy(s1, s2) |
复制字符串 s2 到字符串 s1 |
strcat(s1, s2) |
连接字符串 s2 到字符串 s1 的末尾 |
strlen(s1) |
返回字符串 s1 的长度 |
strcmp(s1, s2) |
返回s1与s2的比较结果. 若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数 |
strchr(s1, ch) |
返回一个指针, 指向字符串s1中字符ch的第一次出现的位置 |
strstr(s1, s2) |
返回一个指针, 指向字符串s1中s2的第一次出现的位置 |
支持的运算符重载: (这是string
的特性还是字符数组自带的特性啊???)
函数 | 功能 |
---|---|
= |
赋值. 不用长度相同 |
+ |
拼接 |
[] |
访问字符串中第i 个字符 |
> , etc |
比较大小. 按字典序 |
常用函数(这是string
的特性还是字符数组自带的特性啊???)
函数 | 功能 |
---|---|
substr(start, length) |
求start 开始长度为length 的子串 |
s1.append(s2) |
把s2 加到s1 的后面 |
s.size() or s.length() |
求长度(有区别吗?) |
C++里最好传引用, 不要传指针. 那是C里面的做法. 如果传递一个不需要被修改的对象, 最好加上const
.
- 定义引用时一定要将其初始化成引用某个变量
- 初始化后, 它就一直引用该变量, 不会再引用别 的变量了
- 引用只能引用变量, 不能引用常量和表达式
引用作为函数的返回值时, 此函数可以被写到赋值号的左边. 此时, 表达式左边那个函数, 即等价于它的返回值
通常全大写并用下划线分词(?)
总的来说, const
为了就是为了防止修改, 因此会对调用做出一些限制
加const
是防止编译错误的一大法宝. 因为在很多编译器里面, 临时对象都是const
类型的, 有时候会不知不觉地使用临时对象
一句话, 常引用是不希望通过引用对内容进行修改. 因此不能用一个常引用对一个非常引用进行赋值(但反过来可以, 或者使用强制类型转换). 因为如果这样, 原本受到常引用保护的目标, 就会被“转引”到一个非常引用上, 失去了保护作用
常量指针不能通过指针修改被指向的内容, 但是指针本身指向的目标可以改变
常量指针和非常量指针的赋值关系和引用类似
常量成员对象不能调用非常量成员函数的值(的值???). 这也是为了防止非常量成员函数不小心把常量对象的值给改了
const
关键字加在参数表的后边
常量成员函数不能修改其所作用的对象, 因此不能修改成员变量的值(静态成员函数除外), 也不能调用非常量成员函数(静态成员函数除外)
如果有两个一模一样的成员函数, 只是有无
const
的区别, 那么这两个函数算重载
因此, 如果只是在类内部声明常量成员函数, 而在类外面写函数体, 那后者也要加const
(否则写的这个函数体就是没有重载的版本)
只要符合常量成员函数的要求, 就最好将成员函数写成常量
如果某个成员你变量是mutable
的, 则它可以被const
成员函数改掉
使用cin
和cout
需要
#include <iostream>
using namespace std;
如果不using namespace std;
则需要std::cin
可能涉及到cin
和cout
重载的问题, 见……
成员变量命名大多以m_
开头(?)
声明成员变量的地方直接初始化变量(in class member initialization)是一种很好的习惯, 会非常直观, C++11之前是不是允许这样操作的, 而C++11之后允许对非静态成员变量进行初始化(in-class initialization)
将成员函数在类体内声明, 而在类体外定义, 是比较好的编程习惯. (因为在类体内定义的成员函数会自动地成为内联函数, 而在类体外不会(?和内联函数有什么关系么))
名字和类名一样. 参数随意, 可以重载, 不能有返回值
注意, 构造函数会且仅会在对象初始化的时候调用, 因此一定要让编译器知道该调用哪个构造函数. 一旦完成了初始化, 和构造函数就没有关系了
注意, 一旦写了自己的构造函数, 如果它不是无参的, 那么此时编译器便不会自动生成的无参构造函数, 此时便没有无参的构造函数了
构造函数最好写成public
的
只有一个参数, 是对同类对象的引用. 可以选择是否加const
, 不过最好加上, 否则容易莫名其妙地编译错误
任何对象都一定有复制构造函数
注意, 复制构造函数也属于构造函数, 如果你写了复制构造函数, 同样会抹掉缺省的无参构造函数. 但是反过来, 如果自己写了构造函数, 它并不会抹掉缺省的复制构造函数
注意, 缺省的复制构造函数确实会执行复制的工作, 但是自己写的复制构造函数, 除非你写了, 否则不会执行复制的工作
复制构造函数调用的三种情况:
- 用一个对象去初始化同类的另一个对象. 注意,
Complex c2 = c1
不是赋值语句, 是初始化语句, 等价于Complex c2(c1)
, 要调用复制构造函数 - 函数的传值调用. 形参是实参的一个拷贝, 这个拷贝过程就要用到复制构造函数. 自己写的复制构造函数可能导致形参不等于实参
- 函数返回某个对象. 从函数内传到函数外(调用它的语句)的时候, 也会调用复制构造函数
其他类型->该类
就是参数表比较特殊的构造函数
类型转换的过程中, 会生成一个临时对象, 这个生成过程中就会调用转换构造函数
explicit
, 显式类型转换构造函数. 如果写了这个, 那么这个转换函数必须被显式地写出来才能执行转换, 而不会自动转换
注意, 类型转换构造函数只能实现其他类型向该类型的转换 (比如, 用一个整型变量对自定义Complex
类对象赋值, 该整型变量会被转换成一个Complex
类的临时对象). 如果要实现该类型向其他类型的转换 (比如, 把一个Complex
类对象转换成整型变量, 只保留其实部), 参见“[转换函数](#(类型)转换函数Conversion Function)”
任何对象生存期结束, 都会调用析构函数. 参数/返回值等临时对象消亡的时候也会调用析构函数
一般只允许有一个析构函数. 缺省析构函数什么也不做
- 对于位于栈上的对象, 其生命周期结束时自动调用析构函数
- 对于被new出来的对象, 它只有被delete的时候才会调用其析构函数, 否则, 即使超出了作用域也不会自行消亡
- 对于对象中被new出来的成员变量 (无论这个对象是在栈上还是在堆里), 一般在其析构函数中写
delete
删除这些成员变量 (避免忘记delete)
该类->内置数据结构
格式: operator typeName() {}
- 转换函数必须是类的成员函数
- 转换函数不能指定返回类型 (返回值类型就是转换目标类型)
- 转换函数不能具有参数 (参数就是他自己)
也可以用来把对象强制转换为指针
类似于Python中的self
C++成员函数, 能看到的参数比实际的参数少一个
其作用就是指向成员函数作用的对象
只有静态函数没有this
指针, 构造函数和析构函数都是有this
指针的
可以delete this
顾名思义, 即为指向对象的指针
通过->
, 从对象指针访问其成员变量/成员函数
注意, 如果是对象名, 则通过.
来访问其成员变量/成员函数
static
静态成员变量必须拿到全局变量的位置上单独声明一下, 是否赋初始值随意 (如果没有赋初始值, 那么默认为0(?))
格式: typeName className::varName = initialValue
, 不能再写static
, 也不用管访问范围
可以通过className::varName
格式调用. 此外, 非静态的调用方式都可以用
注意, 静态成员变量的内存分配是在类外部声明的时候进行的, 即, 如果没有在类外声明, 那么这个静态成员变量是不可以使用的
static
的成员变量, 我自己理解为一个被声明为该类friend
的一个全局变量
本质上是全局函数, 因此没有this
指针 (正因如此它不能作用在任何一个对象上). 静态成员函数函数不能访问非静态成员变量nor调用非静态成员函数, 防止 (在使用className::funcName
格式调用静态函数的时候) 搞不清非静态成员对象是哪个对象的
可以通过className::funcName
格式调用. 此外, 非静态的调用方式都可以用
注意静态成员函数不能是虚函数 (?why)
一个类的成员变量如果是另一个类的对象, 就称之为“成员对象”. 包含成员对象的类叫封闭类(enclosed/enclosing(?) class)
封闭类对象创建的时候, 一定要让编译器知道成员对象是如何初始化的 (初始化列表)
- 封闭类对象生成时, 先执行所有成员对象的构造函数, 然后才执行封闭类自己的构造函数. 成员对象构造函数的执行次序和成员对象在类定义(声明?)中的次序一致, 与它们在构造函数初始化列表中出现的次序无关
- 当封闭类对象消亡时, 先执行封闭类的析构函数, 然后再执行成员对象的析构函数, 成员对象析构函数的执行次序和构造函数的执行次序相反, 即先构造的后析构, 这是C++处理此类次序问题的一般规律
如果用缺省复制构造函数函数初始化一个封闭类对象, 那么它的成员对象也会用复制构造函数初始化
类名::构造函数名(参数表) : 成员变量1(参数表1), 成员变量2(参数表2), ... {}
- 若成员变量是成员对象, 则
参数表n
决定了该成员对象该调用哪个构造函数进行初始化 - 若成员变量是基本类型的成员变量, 那么
参数表n
即为它的初始值 - 前面那个
参数表
, 需要在参数前面加上它的数据类型; 后面那些参数表n
, 直接写参数. 因为前者的参数表是“定义”, 所以需要; 而后面那些是“调用”, 所以不需要
用初始化列表进行初始化, 与在构造函数体内用赋值语句进行初始化相比是更好的风格
成员对象构造函数调用的顺序, 与该封闭类中对成员对象声明的顺序有关
如果想让某个函数(其他类的成员函数or全局(?)普通函数)访问一个类的私有成员, 则需要在这个类中声明友元. 格式为:
friend returnType className::funcName(paraTable)
, for其他类的成员函数, orfriend returnType funcName(paraTable)
, for全局函数
如果类中出现了
friend
, 那它后面跟着的肯定不是这个类的成员
如果类A是类B的友元类, 则类A的成员函数可以访问类B的私有成员. 格式为:
class A {};
class B {
friend class A;
};
友元类之间的关系不能传递(朋友的朋友不一定是朋友), 不能继承(爸爸的朋友不一定是儿子的朋友)
郭炜: “这是对C程序员的妥协”
可以重载为成员函数(参数数量为运算目数减一, 因为已经有this
)或者普通函数(参数数量即为运算符数目)
重载为普通函数的时候仍然可以写在类体内部, 在前面加上
friend
即可
格式: returnType operator OPERATOR(paraTable)
有些运算符不能重载
有些运算符只能重载为成员函数
delete
new
是可以重载的
*
(解引用)是可以重载的
赋值运算符也可以被重载. 浅拷贝和深拷贝
或者是希望赋值运算符两边的类型不匹配(?)
赋值运算符只能重载为成员函数
关于返回值, 赋值号返回值为左值的引用(倒不是一个强制要求, 是一个比较好的风格). 具体实现时, 函数返回类型写左值类型的引用, 返回*this
类名 &operator=(右值) {
//do something
return *this;
}
浅拷贝即把右值逐个比特地拷贝给左值
(对于自行写出来的String类而言, 由于对象中含有指针, 直接拷贝指针, 导致两个指针内容一模一样, 即指向同一片内存空间, 这时)浅拷贝的三个麻烦:
- 它们变成互为“引用”的关系, 改了一个的值, 另外一个也会变
- 原来的内存泄漏
- 被指向的同一片空间可能被delete两次
此外, 除了赋值运算符, 复制构造函数也会涉及到深浅拷贝的问题
cin
是istream
类的对象, cout
是ostream
类的对象, 它们是在iostream
中定义的
一般来说, 重载cin
和cout
是为了实现对自定义类型的操作, 而这个操作很有可能涉及该类型的私有成员, 此时需要在该类中将其声明为友元函数.
函数体可以在类体内, 也可以在类体外. 不过, 在编程填空中, 有时只能在类体内部写代码, 此时需要将函数体写在类体内部(?)错, 此时实际上是重载为友元函数
这里以自定义Complex
类为例. 格式:
class Complex {
double real, imag;
friend istream &operator>>(istream &in, Complex &x) {
in >> x.real >> x.imag;
return in;
}
friend ostream &operator<<(ostream &out, Complex &x) {
out << x.real << '+' << x.imag << "i";
return out;
}
};
in
只是istream&
类的形参的名称, 可以取任何(合法)名字,out
同理. 它们必须写成非常引用- 引用, 是因为它们的复制构造函数是私有的, 函数调用的时候没法复制形参
- 非
const
, 是因为后续要对其进行修改(?)
- 由于这个函数实际上是写在
Complex
类内部的普通函数(而并不是成员函数), 因此不存在一个隐藏的this
指针, 即第一个参数in
或out
需要写出来. 另外, 由于是普通函数, 因此这里无所谓是否为public
- 参数表一般是传引用
- 返回这个对象(的引用)本身, 是为了实现连续读入/输出
- 另外, 如果不需要改变
x
的值, 可以加上const
重载为后置形式的时候要多一个没用的int
函数的返回值一般都是临时变量()
重载[]的时候, 注意其左值和右值的两种情况(?)
++的重载. (++a)=5是可以的, (a++)=5是不可以的. 前者返回的就是&a, 是它本身; 后者返回的是一个临时变量. 所以重载的时候返回值是不一样的
可重用性好
派生类有基类的全部特点(不论private
, public
或protected
), 还可以对基类进行修改或者扩充
注意, 派生类不可以访问基类中的private
成员, 但是这些不能被访问的成员确实在派生类对象的内存中存在
语法:
class 派生类名 : public 基类名 {};
- 这里的
public
指的是公有派生. 另外两种不常用
派生类对象包含基类对象, 而且基类对象的存储位置位于派生类对象之前
虽然派生类包含基类对象, 但是如果在派生类中想访问基类中定义的变量, 则这些变量在基类中不能写成
private
覆盖: 如果派生类中定义了和基类中同名对象/函数(对于函数还需要同参数表)(?), 则为覆盖. 访问成员对象/函数的时候默认访问的是派生类中的那个. 如果派生类中没有, 而基类中有, 则访问基类中那个. 如果都没有则报错. 如果想指定访问基类还是派生类中的对象/函数, 则需要类名::
一般不会在基类和派生类中写同名变量, 但是写同名同参数表函数是很常见的
派生类的构造函数中, 要用初始化列表说明基类应该调用哪一个构造函数. 有点类似于封闭类的构造函数
如果派生类也是一个封闭类, 则先调用它的基类的构造函数, 再调用成员对象的构造函数, 最后执行自己的构造函数
- 派生类的对象可以赋值给基类对象
- 派生类对象可以初始化基类引用
- 派生类对象的地址可以赋值给基类指针
- 注意, 如果一个基类指针指向了一个派生类对象, 那也不能通过这个指针访问基类中没有而派生类中有的对象. 如果你确定了这个基类指针一定指向某个特定的派生类对象, 那么可以对指针用强制类型转换.
virtual
, 成员函数, 只用声明的时候写, 定义的时候就不用了
若基类中声明了虚函数, 则其派生类的同名同参数表函数自动成为虚函数. 但是反过来不行, 即如果一个派生类中把某函数声明为虚函数, 那么并不会向上追溯到它的基类将其同名同参数表函数变成虚函数(?)
构造函数和静态成员函数不能是虚函数, 但是析构函数可以. 所以, 如果在构造函数的时候需要用到多态的功能, 那就用switch
吧.
析构函数中调用虚函数, 不是多态(不是多态????)
虚函数可以参与多态
当一个基类指针/基类引用指向/引用了一个派生类对象时, 若通过该指针/引用调用其虚函数, 则为多态. 即, 实际指向/引用的是什么类, 就调用那个类的虚函数
如果本该调用该类的虚函数, 但是该类中并没有定义该虚函数, 则向上调用其基类的同名同参数表函数(?)
如果调用的不是虚函数, 那么, 调用哪个类中的函数, 取决于指针/引用其本身的类型, 而不是其指向/引用的对象的类型
在非构造函数/非析构函数的成员函数中调用虚函数, 是多态. 因为调用虚函数的语句, 实际上等价于有一个this
指针
用基类指针数组指向一系列(被new出来的)派生类对象, 再遍历这个数组, 对各个不同类型的对象进行操作, 是一个很常见的做法. 对这个指针数组(所指向的对象)进行排序, 可以用自定义的compare函数, 再调用sort
虚析构函数可以使得通过多态调用派生类的析构函数的时候, 在执行派生类的析构函数后, 继续执行其基类的析构函数
如果一个基类指针指向了一个派生类对象, 如果没有把析构函数写成虚函数, 那么delete的时候, 只会调用基类的析构函数
如果一个类定义了虚函数, 则析构函数也要定义成析构函数. Or to say, 如果一个类打算作为基类使用, 则应该将其析构函数定义成虚函数
连函数体都没有, 直接 = 0
. 注意只有虚函数才能这么写, 这么写的话. 这个函数就实际上不存在.
…这么做实际上就是等它的派生类将这个函数实现, 但是又能在基类指针中调用这个函数而不报错.
包含纯虚函数的类称之为抽象类
抽象类不能创建对象, 仅能当作基类供派生使用
当一个派生类继承自一个抽象类, 只有它实现了基类的所有纯虚函数的时候, 它才不再是一个抽象类
抽象类的成员函数中可以调用纯虚函数, 但是构造函数和析构函数中不可以调用纯虚函数 (因为这俩不是多态, 需要执行自身的函数, 但是纯虚函数没有函数体; 而在有多态的情况, 就一定会执行派生类的函数)
类和类之间的关系:
- 复合关系(封闭类). “有”关系. 是固有成分或者组成部分. 如, 汽车中包含轮胎和发动机
- 继承关系. 逻辑上要满足“是”的关系. 如 , 一个中学生是一个学生
(写两个类的时候, 要考虑一下它们之间的关系)
指针指来指去的方法也可以被称作一种复合关系. 这个关系有些人称作“知道”
注意, 互相通过引用调用函数的时候, 注意定义的完整性(?). 即, 最好在类体内部只写函数声明, 而把函数体放在这些互相知道的两个类的后面去写
没有结尾的\0
可以用字符数组初始化, 或者无参初始化. 但是不能用单个字符初始化(必须先用别的方法初始化, 再把单个字符赋值给这个字符串)
定长数组
可变长数组
注意, 虽然.emplace()
和.push_back()
没啥太大差别(而且前者的性能更好), 但是如果要插入的内容是一个指向new出来的对象的指针的时候, 只能用后者(?)
set
中元素的排序是从小到大, 即任意两个元素放进小于号或者比较函数中能使得其值为true
快速初始化
int a[5] = {1, 5, 3, 4, 3};
set<int> b(a, a + 5);
vector<int> c{1, 5, 3, 4, 3};
set<int> d(c.cbegin(), c.cend());
lower_bound
: >=
upper_bound
: >
/*
supposing a multiset like this: [1, 1, 2, 2, 3, 3]
and we look for `2`
then the lower and upper bound should be like this:
v lower_bound
[1, 1, 2, 2, 3, 3]
^ upper_bound
*/
map<keyType, valueType> varName
注意keyType
必须是一个可以用小于号的类(即, 如果是自定义的类, 必须重载小于号)
注意map
中存贮的对象为pair
. pair
也是一个类模板
mapName.insert(pair<keyType, valueType>(key, value))
mapName.insert(make_pair(key, value))
mapName[key] = value
mapName[key]
用make_pair
也行
缺省情况下用的是容器类的operator<
. 想要自定义排序方式, 可以:
-
在创建该容器的时候指定排序方式(仿函数)
struct Comp { bool operator()(const MyClass &lhs, const MyClass &rhs) const { // make a judgement } };
此时判断的是
lhs < rhs
注意, 如果
Comp
是模板或者Comp
后面的()
内跟了参数, 那个是初始化用的参数, 而非operator()
用到的参数. 即用这些参数初始化出一个Comp
类的对象, 之后每次判断两个元素大小的时候都调用一下operator()
-
重载这个类的
operator<
bool MyClass::operator<(const MyClass &rhs) const { // make a judgement }
注意, 此时
*this
即为lhs. 判断的是*this < rhs
若有vector<int>
, 则其迭代器类型为vector<int>::iterator
new
表达式的返回值一定是一个指针. 它在堆上new一个新的对象出来, 并返回它的地址作为指针
对数组使用delete
的时候, 注意加[]
int *p = new int; //分配1个int型的内存空间
delete p; //释放内存
int *p = new int[10]; // 分配10个int型的内存空间
p[5] = 1; // 用下标访问
delete[] p;
当然, 也可以先初始化指针, 再用new对其进行赋值
用下标访问动态分配的数组时, 可能存在越界的问题, 而且编译的时候不会报错! 因此使用的时候要格外留意(或者干脆将其封装, 并在封装类内部检查是否越界)
注意, 下文只是给出了其实现方法. 实际操作中, 分配一维数组并实现二维下标和一维下标的互换, 是更好的实现方式.
// 指定高度和宽度
int h = 3;
int w = 4;
// 动态分配内存
// 先new出长度为h的int*类型的数组
int **p = new int *[h];
// 再对每一个int*, new出长度为w的int类型的数组
for (int i = 0; i < w; ++i) {
p[i] = new int[w];
}
// 访问&赋值
// 特别注意, 访问时不会检查下标是否越界
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
p[i][j] = 10 * i + j;
}
}
// 访问&输出
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
cout << p[i][j] << ',';
}
cout << endl;
}
// 如果只用一个中括号, 即只指定行而不指定列,
// 那么访问的是该行的第一个元素(或其地址)
cout << *p[2] << endl; //输出20
cout << p[2] << endl; //输出其地址
// 释放内存
// 先释放内层, 再释放外层
for (int i = 0; i < w; ++i) {
delete[]p[i];
}
delete[]p;
使用格式如下
template <class 类型名, ...>// 类型参数表
返回值类型 模板名(参数表) {}// 注意, 这里叫模板名, 不叫函数名
模板是模板, 函数是函数, 模板实例化才得到函数
调用模板的时候, 可以模板名(参数表)
, 通过参数表给出的类型, 隐式给出实例化的方式, 也可以模板名<类型参数表> (参数表)
, 直接给出类型参数表, 来显式地实例化
函数模板也可以重载
- 完全匹配的普通函数
- 完全匹配的模板函数
- 自动转换后匹配的普通函数
总之, 记住模板函数不会自动类型转换(要么就彻底乱套了)
通常写函数模板的时候Pred
用来代表函数指针
把函数写到函数模板的参数表的时候, 只写一个简单的类型参数就行, 它会自动化实例化成函数指针的类型
一个函数指针的类型为返回值类型 (*) (参数表)
, 它不等于返回值类型
或返回值类型 (*)
经典应用: 编写一个可变长的数组类
语法:
template <class 类型名, ...>// 类型参数表
class 类模板名{};
类模板的成员函数(在类体外)的写法:
template <class T1, ...>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表) {}
调用的时候也和上文类似
…里面的“类型参数名列表”好像只用给出数据类型, 而不用给出形参名
用类模板定义对象的写法:
类模板名<真实类型参数表> 对象名(构造函数实参表);
…注意其中的“真实类型参数表”可以被省略, 此时会根据实参自动确定类型
同一个类模板实例化出来的模板类是不同的类(不兼容的)
比如
vector<int>
和vector<double>
是不同的类
类模板和函数模板可以互相套娃
类模板可以声明友元, 从这个模板中实例化出来的任何类都有这些友元
类模板中可以把函数模板声明为友元, 这个函数模板实例化出来的任何模板函数在这个类模板实例化出来的任何模板类中都是友元
类模板可以写静态成员变量, 从这个模板中实例化出来的任何类都有这些静态成员变量 (而且不同类的静态成员变量是不互通的)
注意一下静态成员变量需要单独声明一下
cin
是istream
的对象. cout
是ostream
的
表达空位用NULL
(?)
int
右移高位补1(?), unsigned int
右移高位补0. 所以尽量用unsigned int
要把左边$n$位置零, 可以先左移再右移$n$位. 右边置零同理
如果要把中间若干位置零, 可以弄两个数组把左边和右边各自一段长度分别置零, 再|
void *memcpy(void *destin, void *source, unsigned n)
函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中, 即从源source
中拷贝n
个字节到目标destin
中
注意是n
个字节, 因此在拷贝数组的时候, n
为待拷贝的数组长度$\times$每个单位的字节数(用sizeof)
需要#include <algorithm>
, 是stl
实际上是C的库函数
需要#include <iomanip>
固定宽度输出: 先用setw()
设定宽度, 再用setfill()
设定填充字符
保留小数位数: 先fix
or scientific
, 再setprecision(n)
不同进制输出
setprecision:
- 非定点输出(默认情况): 有效数字位数
- 定点输出(小数点左边就是个位): 小数位数
- 整型数据不受影响
setiosflags(ios::fixed), 强制定点输出. 以及resetiosflags
fixed, 也可以用来指定定点输出
scientific, 强制科学计数法输出, 但是保留的是小数点后的位数
setw()
. 宽度设置是一次性的. 注意, 如果写cin.width()
, 那么要多一个长度用来放\0
默认用空格填充在左边
left, right, 表示左对齐/右对齐
用户可以自己定义流操纵算子
#include <alogrithm>
int a[5] = {7, 3, 8, 2, 4};
cout << *min_element(a, a + 5) << endl;
cout << *max_element(a, a + 5) << endl;
定义是声明, 声明不一定是定义
声明可以有很多个, 定义只能有一个
函数原型即为函数的声明, 它不包含函数体, 即后面不用写花括号, 而是直接用分号结尾. 函数原型中的参数表只需要指定参数格式(交互接口), 不需要写出形参名
用来确定类型
返回一个type_info
类型的对象
判断某个对象是否属于某个类, 可以这么写: typeid(对象名) == typeid(类名)
注意, 对于基类&派生类而言, 如果基类中不存在虚函数, 则对指向派生类的基类指针/引用用typeid
的时候, 在编译时即可确定(没有动态联编); 如果要实现多态, 则需要在基类中写虚函数.
注意指针的类型和指针所指向的对象的类型的区别. 以CWarrior
和CLion
为例:
class CWarrior {
public:
virtual ~CWarrior() {}// 虚析构函数, 用来实现多态
};
class CLion : public CWarrior {
};
int main() {
CWarrior *p = new CLion;
if (typeid(*p) == typeid(CLion)) cout << "YES!" << endl; else cout << "NO!" << endl;// YES!
if (typeid(p) == typeid(CLion *)) cout << "YES!" << endl; else cout << "NO!" << endl;// NO!
if (typeid(*p) == typeid(CWarrior)) cout << "YES!" << endl; else cout << "NO!" << endl;// NO!
if (typeid(p) == typeid(CWarrior *)) cout << "YES!" << endl; else cout << "NO!" << endl;// YES!
return 0;
}
基类函数的虚函数不要写成私有成员, 要么编译的时候会出错(编译的时候不会分析程序结果, 看到你调用的是基类函数就直接报错了)
注意, 这个时候, 如果基类是公有, 而其派生类中同名同参数表函数是私有, 通过基类指针指向派生类对象也可以调用派生类的私有函数
公有私有, 类型检查, 只会在编译的时候检查. 只要编译过了, 那就不存在这些问题了
输入输出
cerr
: 不用缓冲区
clog
: 先放在缓冲区
判断输入流结束
while (cin>>x)
输入重定向
freopen(“filename”, “r”, stdin), 指标准输入被重定向到这个文件了
stdout, 输出重定向
getline
小心对\n的处理
istream的其他成员函数
文件读写
文件也可以看作字符流. 注意, 只有文本文件(与之相对的是二进制文件)能够用cin cout读写
#include <fstream>
要先创建一个ofstream类的对象
或者, 可以先创建, 再用open打开
关于路径. 如果是以\\
开头, 那么指的是从当前盘符的根目录开始
..\\
, 指父目录, 可以多层嵌套
文件的读/写指针, 标识文件操作的当前位置. 位置用偏移量表示, 表示离文件开头有多少个字节
tellp取得指针的位置
seekg
seekp
显式关闭文件
迭代器的数据类型
举例: vector<int>::iterator
, 需要指明容器类型以及容器中元素的类型
当然我这是因为要写在成员中, 因此必须这么写. 平时写auto
就好了
访问迭代器的返回的元素, 需要用*
(再次印证迭代器就类似一个指针)
当需要得到$2^n$的时候, 直接1 << n
就好, 不需要#include<cmath>
再pow(2, n)
输出空行也可以用puts("");
oj就不要动态分配数组了, 开一个足够大的数组+避免越界就行
memset()
- 需要
#include <cstring>
vector
比较函数用来传进去的比较对象怎么写
调用这个对象的时候, 实际上是通过圆括号即operator()
调用的. 但是创建对象的时候, 要用构造函数.