照学校课件打的,大概率没什么意思,有空再修改。
C++ 程序结构
多文件编译
将源代码文件放一块儿就行。
1 | g++ a.cpp b.cpp c.cpp -o program |
包含警戒
保证头文件不被重复定义。
解决方案一:#pragma once
解决方案二:包含警戒。
1 | // a.h |
extern
- 声明其它文件中的变量或函数。
extern "C"
:要求编译器按照 C 语言的方式编译函数(C++ 为实现函数重载会调整函数名,链接时可能出现问题)。
类型和变量
内置类型
wchar_t
:wide char type
推导类型
auto
:定义时自动推导类型。decltype(expression)
:给出表达式的返回类型。
typedef 函数
用法:typedef return_type (*new_name)(type...)
1 |
|
enum 枚举类型
默认从零开始,名称作用域为全局,不能前置声明。
1 | enum Week { Mon = 1, Tue, Wed, Thur, Fri, Sat, Sun }; |
enum class 类型
作用域不再是全局,需要
::
才能访问。不能进行隐式类型转换。
可以前置声明,如果类型不是默认的
int
需要手动修改enum class Name: std::uint32_t;
.
enum class
不能与 int
相互转化。注意命名空间。
1 |
|
union 共用体
1 |
|
(这样取一些位会很方便,然而 sizeof
是不变的……)
引用类型
左值引用:貌似不能修改引用对象的指向。
右值引用:不算清晰,大概就是可以对临时对象进行操作?
初始化(C++11)
1 | struct Type { int a, b; }; |
static 关键字
- 函数内表示静态变量。
- 函数外表示仅本文件有效。(不同文件重名会被认为是不同变量,由编译器处理)
指针、数组、引用、常量
指针
NULL
:C++ 中就是整数 0(C 中是(void *)0
)。nullptr
:C++11 中用来避免函数调用时产生二义性。
见:https://blog.csdn.net/u010983763/article/details/53667468。
deference(解引用/逆向引用):起这么厉害的名字,其实就是访问指针中的元素 *p
。
- 指针数组:
int *arr[5];
,一个数组,存放指针。 - 数组指针:
int (*p)[5] = &array;
,一个指针,指向一个数组,通过(*p)[i]
取值。
宏
#x
将x
表示为字符串。##x
将x
与前面拼接。#@x
将x
表示为字符。__LINE__
表示当前行号。__FILE__
表示当前文件的绝对路径。__FUNCTION__
表示当前所在的函数名。(调试时应该会很有用)
常量折叠
const
定义的常量不会被重复定义(编译时被展开)。
指针常量
const int *p = &arr;
:指向的内容不能修改。int* const p = &arr;
:存放的指针不能修改。const int* const p = &arr;
:俩都不能改。
未解之谜:
1 | const int *p = &arr; |
const 引用
1 | const int &a = 1;//正确 |
函数参数类型是 const int &x
表明可以传入变量或常量(常数也可),在函数体内不能修改而已。
字符串相关
这两者是不同的:
1 | char *s = "string";// 是指针,因此可以修改指向 |
md,C++ 好难……
函数
调用方式
__cdecl
:C 模式,参数从右到左压栈,主调用的函数控制,参数可变,可不指定参数名。__stdcall
:参数从右到左压栈,被调用的函数控制,参数不可变。
关于不指定参数名:
1 | void __cdecl print(int) { |
只有函数体中用不到该形式参数时可以省略,这种情况下 C 和 C++ 有一些不同:
- C 语言在声明中可以省略,定义中不可以省略。
- C++ 在声明中可以省略,定义中也可以省略。
函数重载
缺省函数(调用时使用默认参数):
1 | void func(int a = 100) { |
- 缺省函数不能用来区分同名函数。(缺省函数还可能存在其它歧义行为,均被禁止)
- 返回值类型不能用来区分同名函数。
- 引用/指针类型(值类型不行)的
const
可用于区分同名函数:
1 |
|
关于缺省函数:第一个带缺省值的参数后的每一个参数都要带缺省值(不然会有歧义)。
【上面第三条】关于 const
形参(参数必须是引用/指针类型):
- 实际参数无
const
,形式参数有无const
都可调用,优先调用无const
的函数。 - 实际参数有
const
,只能调用形式参数有const
的函数。
1 |
|
汇编逻辑
函数调用时数据存放在栈中,自顶向下从高位到低位,EBP 寄存器用于指向该函数的栈底,ESP 指针则指向栈顶,上一层函数 EBP 寄存器的值往往存放在当前函数的栈底。
返回值
- 值类型:都是
const
,等价于const int f();
- 指针类型:指针
const
,指针内容可变,等价于int* const f();
(不是const int* f();
)。 - 引用返回:
int &f();
,如果要返回的是一个很大的类那就十分有用了。
1 |
|
写成这样也是可以的:
1 |
|
而改为:
1 | const int &func() { static int a = 1024; printf("%d\n", a); return a; } |
就会报错。
类
前置声明
1 | class Type; |
避免循环定义。
静态变量
类的静态变量只能在类外初始化,类中只能初始化 static const int
类型,去掉 const
或者不使用 int
类型均会报错。
static const int
:允许类内初始化。static constexpr
:必须类内初始化,不能只声明。static
关键字的其它类型:不允许类内初始化,仅可以在类内声明。
据说是为了避免每个对象中都包含该静态成员变量。
1 | class Type { |
注:constexpr
将尽量在编译阶段运算。
虚函数(virtual)
调用成员函数时,虚函数由指针指向的实际类型决定,普通函数由指针类型决定。
简单来说就是普通成员函数都放在代码区,取的时候按照指针类型去取;而虚函数在对象中保存虚函数表指针 vptr,通过 vptr 找到虚函数表 vtbl,再通过 vtbl 找到对应的虚函数,因此由对象的实际类型决定。(包含虚函数的类占用的空间会更大)
构造函数不能是虚函数,而析构函数最好设置为虚函数(析构所有的新成员)。
(纯虚函数 virtual void func()=0;
用于抽象类,即抽象函数)
内联函数
inline
关键字,类内实现的函数都默认内联,类外实现的函数须加关键字实现内联且不能放在代码文件中(需要放在头文件中)。
【内联函数的一些问题】待填坑。
访问权限
没有指定访问权限的成员变量默认私有。
public
:公开protected
:子类可访问private
:仅自己或友元可访问
常成员函数
写法:void func() const {}
- 普通函数的
this
相当于T* const this
:指针不可变,指向内容可变。 - 常成员函数的
this
相当于const T* const this
:指针不可变,指向内容不可变。
如果对象是 const
类型的话,不修改对象的成员函数若不指定为 const
函数则无法调用。
类函数/类变量
与 Java 中类似,加 static
关键字,最好使用 ClassName::FuncName()
而不是 ObjectName.FuncName()
调用。
- 没有
this
指针。 - 没有
const
修饰。(没有对象谈何对对象不做改动)
构造函数与析构函数
explicit 关键字
不允许奇怪的隐式类型转换,像 Point p = 1
这种。
一些无聊的尝试
定义 static
类函数,使用引用 &
或指针返回创建的对象(一种是 new
到堆里,一种是 static
到静态数据区【这种方法只能创建一次】)。
构造函数
当类有带参数的构造函数时,不能不初始化(ClassName object;
)。
构造函数其实是在对象创建之后才调用的。
初始化
类变量只有整型常量可以初始化,这个是前面提到过的 static const int
。
在参数列表里初始化引用对象和常量,这样是没问题的:
1 | int val = 1; |
初始化列表将严格按照定义的顺序初始化。
初始化列表先于构造函数的代码块执行。
析构函数
没有参数:~ClassName() {}
析构函数则是在对象销毁之前调用的。
拷贝和赋值
返回值存放在栈中,使用时再拷贝出来。(返回的不是单个整数时编译为何种汇编语言?实验没有成功进行因为被编译器所优化)
拷贝构造函数
默认存在。
调用:
Type b(a);
Type b = a;
(好像不太推荐)
自定义(需要引用符号,引用传递):
- 不修改被拷贝的对象:
Type(const Type &rhs) {}
- 要转移被拷贝的对象:
Type(Type &rhs) {}
参数列表调用:
- 这样调用的是拷贝构造函数(无论加不加引用符号):
Type(Type &a):a(a) {}
。 - 这样调用的是普通构造函数:
Type(int a):a(a) {}
。
【为什么拷贝构造函数不能值传递?】值传递时会在函数执行时创建局部变量,该局部变量初始化时需要使用拷贝构造函数将实际参数的值赋给它,这样再次调用拷贝构造函数就会产生死循环。
浅拷贝/深拷贝
浅拷贝:成员变量是一个对象时调用其对应的拷贝构造函数,其余(基本数据类型,指针类型,引用类型)直接按二进制拷贝。
深拷贝:自己实现,拷贝的不再是指针,而是指针内的值。
有构造函数 | 无构造函数 | |
---|---|---|
有拷贝构造函数 | 无默认构造函数和拷贝构造函数 | 无默认构造函数和拷贝构造函数 |
无拷贝构造函数 | 有拷贝构造函数 | 有默认构造函数和拷贝构造函数 |
即没有什么提供什么,但自定义的拷贝构造函数就可以代替构造函数。
禁止拷贝
- 自定义一个没有实现
{}
的private
的拷贝构造函数。 - C++1z:
T(const T& t)=delete
。
拷贝与赋值
- 拷贝:
Type a = b;
- 赋值:
Type a; a = b;
【有引用类型的成员不能赋值,可以拷贝】拷贝是初始化的过程,而赋值时引用类型的成员已经有引用的变量,因其不能更改所以不能赋值。(成员变量为引用类型:须自定义构造函数和拷贝构造函数,不能赋值。)
自定义赋值函数:重载 =
运算符 Type &operator = (const Type &rhs) { return *this; }
(参考内置类型的使用习惯)。
课件中一个有趣的代码:
1 |
|
此时编译正常,但逻辑错误,应当判断 (&rhs != this)
后再进行删除和赋值。
运算符重载
只能重载一元运算符和二元运算符:,
、&
(与运算)、->
、new
、new[]
等。
两种形式:自由函数 AND 成员函数。
访问权限等参考内置函数,如:
T &operator *= (const &T rhs) {}
T operator * (const &T rhs) {}
bool operator ! () {}
T &operator ++ () {}
:对应++t
,返回的是自加后的值,因此要加&
符号。T operator ++ (int) {}
:对应t++
,返回的是自加前的值,因此无需&
符号。int opeartor [] (int index) const { return arr[index]; }
int &operator [] (int index) { return arr[index]; }
int operator () () const { return 1024; }
:仿函数,可用 lambda 表达式代替。- 重载
->
运算符须保证:返回指针类型或返回的自定义类型重载了->
。 ostream &operator << (ostream &out, const T &t) { out << t.a; return out; }
istream &operator >> (istream &in, const T &t) { in >> t.a; return in; }
注:lhs
、rhs
分别是 Left Hand Side 和 Right Hand Side 的缩写。
左操作数是自定义类型的尽量使用成员函数的形式重载,不要重载运算符 &&
、||
、,
。
动态内存管理
动态内存管理:存放在全局堆区(new
申请的内存)
new/delete
分配数组类型:int *p = new int(5)
分配指针类型:const T **p = new P*();
释放数组类型:delete[] arr;
new
失败后会跳转 new_error_handle()
函数(可通过 set_new_handler()
修改),成功会执行构造函数并返回对应类型的指针(重载 new
运算符返回的 void *
类型指针会强制转化为 T *
类型指针)。
1 |
|
重载 new/delete
void* operator new(std::size_t);
对应 new T(1024);
void* operator new(std::size_t, const string &s);
对应 new("string") T(1024);
void operator delete(void *, std::set_t);
对应 delete p;
动态数组
1 | int* arr = new int[n];// n 可以为变量 |
普通类型对象数组 Type arr[1024];
要求必须有无参构造函数。
指针类型对象数组 Type* arr[1024];
不要求必须有无参构造函数。
auto_ptr
将指针封装在类中,以便析构时自动释放。
shared_ptr
共享指针,记录指针被引用的次数。
<memory>
std::shared_ptr<Type>
std::make_shared<Type>()
p.get()
*p
等价于*p.get()
p.use_count()
p.reset()
1 |
|
(printf
中输出地址用 %p
)
1 |
|
可以像普通指针一样用赋值号,也会被累计其中哦!
函数体结束就释放了:
1 |
|
除非作为返回值:
1 |
|
这个东西可以利用前面学到的拷贝构造函数和重载赋值自己实现。
乱七八糟概念
悬浮指针:释放后访问指针。
内存泄漏:申请了没释放。
写时复制:指向多个同一资源,需要修改的时候才复制一份分开,用来节省空间(有点像可持久化)。
定位分配:分配到已有空间,就是下面这个东西(需要 <new>
,在后面指定分配到的地址)。
1 |
|
转换函数、命名空间、友元、嵌套类、流
类型转换
基本数据类型2自定义类型:构造函数(可通过 explicit
禁止)
自定义类型2基本数据类型:
1 |
|
(C 风格强制类型转换 (Type)Value
和 C++ 风格强制类型转换 Type(Value)
应该没什么大的区别)
命名空间
耳熟能详的东西:namespace NAME {}
。
没见过的东西:::
单独出现表示全局命名空间,否则表示当前命名空间。
可以嵌套:namespace A { namespace B {} }
。
可以起别名:namespace A = B;
。
1 |
|
static
访问域:仅能在当前文件中被访问。
奇怪的东西:匿名空间 namespace {}
,看看它怎么回事。
1 |
|
如果有了全局变量,优先用全局变量
1 |
|
对于匿名空间,编译器会给它一个名字并且自动 using namespace XXX;
,它和 static
具有相同的 internal 链接属性,只对本文件可见,C++ 更提倡使用匿名空间而不是 static
。
using
关键字用于汇入当前命名空间。
嵌套类
嵌套类亦有所谓的访问权限(public
或 private
)。
注:没有指定访问权限的成员变量默认私有。
1 | class A { |
友元 friend
友元函数/友元类:允许访问类的私有成员,写在类的开始,不指明访问权限。
一种很不优雅的妥协,friend关键字只能用在类中,表明允许其后的函数/类访问该类的私有元素,这个函数/类不在当前类中,只需照常使用,额外增加了访问权限而已。
流
字节流/字符流/文件流,istream
和 ostream
。
类间关系
强关联/硬关联/弱关联/软关联:???
联系强弱:(另一类作为)成员变量 > 函数参数和返回值 > 函数实现(局部变量)
关联关系
一般关联/自关联/关联类(设定一个类)/聚集关联
关联关系:聚合关系(不负责创建和销毁)/组合关系(负责创建和销毁)
依赖关系
补充:UML图
- +(public)、-(private)、#(protected)
- 抽象类和抽象方法用斜体
- 接口 <<interface>> 下面是接口名。
- 类实现接口:箭头(空心三角和虚线)
- 类继承父类:(子类向父类)箭头(空心三角和实线)
- 关联关系(成员变量是一个类):(指向成员变量的类)箭头(角和实线)
- 依赖关系(成员函数的参数/返回值是一个类):(指向用到的类)箭头(角和虚线)
- 聚合关系
has-a
:关联关系的特例,箭头(空心菱形和实线角) - 组合关系
contain-a
:关联关系的特例,箭头(实心菱形和实线角)
类的设计
继承/封装/多态
依赖(参数或返回类型)/关联(成员变量)
继承
可以多继承,不继承父类的构造函数、析构函数、拷贝构造函数、赋值函数、类型转换函数,但在构造函数执行前会先执行父类的构造函数。
继承方式:public、protected、private(默认),继承后的访问权限根据继承方式进行相应的削弱(如 protected 继承方式将父类中 public 成员削弱为 protected)。
【private 继承为什么还要存在?】待填坑。
如何调用父类的构造函数?在初始化列表中调用:
1 |
|
为什么 C++ 中没有 super?因为 C++ 支持多继承啊,super 指的是哪个呢?
再给个例子:
1 |
|
注意代码中 V
类型的 rhs
可以调用 P(rhs)
以及 P::operator=(rhs)
。
注意这里类型转换函数直接调用了父类的。
子类的一系列操作
- newdefine:子类中定义,父类中没有。
- redefine:子类中定义,父类中也有。
- overload:子类中定义多个,重载。
- overwrite:子类中定义,父类中有同名的,被隐藏。
- override(和我之前以为的 override 不太一样):子类中定义,父类中为同名虚函数。
继承和类型转换
protected/private 向上(子类转父类)转换:先取地址再转对应类型的指针,不推荐。
public 向上转换:安全的,会创建一个新的对象,所以尽可能使用指针或引用。
1 |
|
public 向下转换:可能出错。
乱七八糟的类型转换操作符
static_cast<T>(exp)
const_cast<T>(exp)
:修改 const
或 volatile
约束。
1 |
|
然后尽管这个函数是 const
,这个对象的值还是被改了。
reinterpret_cast<T>(exp)
:重新解释,用于函数。
dynamic_cast<T>(exp)
:用于多继承时父类转子类。
%%%多重继承
顺序:构造顺序相同,析构顺序相反。
菱形结构多重继承产生重复:虚继承(继承类加 virtual
关键字修饰),被继承的类称为虚基类。
工具类
放置静态函数和静态变量。
接口类
C++ 没有 interface,通过纯虚函数(抽象函数)实现,指定 virtual
之外还要制定函数 =0
。
虚机制
静态编联与动态编联
静态编联:编译期间决定调用关系。
动态编联:执行期间决定调用关系,虚机制是实现方式之一。
虚指针
我以为是个新东西,其实就是 vptr。
虚函数
声明时需要加 virtual
,定义时可加可不加。
- 静态函数显然不能是虚函数。
- 如果有虚函数,析构函数应当是虚函数。
- 构造函数和拷贝构造函数不能是虚函数,赋值函数通常不定义为虚函数。
【为什么构造函数不能是虚函数?】调用构造函数需要 vptr,此时对象还没有实例化,找不到 vptr。
【为什么赋值函数不要定义为虚函数?】因为子类不继承父类的赋值函数(参数类型不同)。
继承
- public 继承,且会继承父类的虚函数。
- 重写函数(函数名相同,多数要求参数相同,返回类型相同或相容)默认
virtual
可省略(包括析构函数)。
虚函数表
- 一个类只有一个(实例化首个对象时创建),所有对象共享。
- 父子类的虚拟表中相同函数的位置相同。
静态类型与动态类型
静态类型:编译期间确定的类型(对应指针的类型)。
动态类型:执行期间确定的类型(对应指针指向的实际类型)。
【重要】如何执行?
- 在静态类型(定义的指针类型)中寻找对应函数,找不到编译错误。
- 若找到的函数不是
virtual
函数则p->P::func()
,若为virtual
函数则根据 vptr 执行(*p->vptr)[index]((void *)p, ...)
。
在构造函数中调用虚函数当然调用的对应类自己的那个函数,由于 B 类继承自 A 类,构造 B 类时也会构造 A 类(注意这个 A 类仅用于创建的 B 类对象,每再创建一个 B 类会就会再执行一次 A 类的构造函数。
B 类对象构造的过程:
- 构造 A 类
- 构造 B 类
B 类对象析构的过程:
- 析构 B 类
- 析构 A 类
1 | /* |
私有的虚函数访问
指针是父类类型时,在父类中调用一个子类中重写过的虚函数(父类中当然定义了),实际上调用的是子类中的那个版本,即便它是 private
的,依然能够执行。(跨类访问私有函数)
具体类和抽象类
抽象类:有纯虚函数(virtual
函数 =0
),也叫抽象函数。
具体类和抽象类的子类既可以是具体类也可以是抽象类。
纯抽象类:除了静态、构造、析构函数均为纯虚函数。
接口类:纯抽象类,成员均为 public static
。
【!】纯虚函数可以在类外给出定义(当然可以不定义),例如:
1 |
|
当然,抽象类不能被实例化,所以个人感觉没什么意义。
继承自抽象类后没有实现对应的抽象函数子类就还是抽象类。
其它
typeid(exp_or_type)
:类型比较。
dynamic_cast<T>(exp)
:父类转子类,T 为指针类型失败返回 nullptr,T 为引用类型失败产生异常。
关于虚函数一些例子
1 | /* 父类定义为虚函数后,子类重写时 virtual 关键字可省略 */ |
1 | /* 仅仅定义子类重写的函数是虚函数无效 */ |
1 | /* 父类类型的指针不能用于访问子类中特有的函数 */ |
其它
越想越觉得虚函数和抽象函数并不是完全没有联系,virtual
关键字就好像告诉我们尽可能地不使用当前类的代码而去使用子类的实现,不同之处在于抽象函数不允许子类没实现而虚函数给了一个子类没实现时的保底。
多态性
定义:相同的消息请求,执行不同的代码体,产生不同的结果。
静态多态:根据静态类型确定执行的代码,如模版和函数重载。
动态多态:根据目标对象动态类型和参数表静态类型确定执行的代码,如虚机制。
使用虚机制
面向对象编程,使用父类指针执行子类代码。
拷贝构造
拷贝构造函数不能是虚函数,静态类型和动态类型不同,如何复制呢?
1 | /* |
问题
如果有多个函数,每个函数有若干种实现,则子类个数为它们相乘。
【解决方案】在类中定义三个变量指向三个新类,每个新类定义一个虚函数,通过继承该类实现这个函数,调用函数时调用对应成员变量的成员函数即可,这样子类个数为各个函数实现方案数的和。
面向对象程序设计
懒得看,鸵鸟法可知这一章没什么东西(doge
一些好玩的
【开玩笑的】隐藏运算符:趋向于(雾):
1 | int t; scanf("%d", &t); |
参考资料
1 |
|
函数省略形式参数名:https://blog.csdn.net/C0631xjn_/article/details/127280342
类中静态变量初始化:https://blog.csdn.net/qq_50868258/article/details/123139071
类中静态变量初始化:https://blog.csdn.net/sevenjoin/article/details/81772792
虚函数:https://zhuanlan.zhihu.com/p/28530472
explicit 关键字与离谱的 C++:https://zhuanlan.zhihu.com/p/52152355
shared_ptr:https://zhuanlan.zhihu.com/p/547647844
UML:https://zhuanlan.zhihu.com/p/109655171
虚继承内存空间安排:https://blog.csdn.net/SuLiJuan66/article/details/48897867
虚继承:https://zhuanlan.zhihu.com/p/104344453
待补充
private构造函数
重载区分
拷贝构造函数的循环