1、用include<iostream>包含文件后,在编译时,iostream文件内的内容就会被拷贝粘贴放入当前的文件中include该行的位置。
2、当使用自定义函数时,不需要include包含函数定义的文件,只需要在头部加上该函数的声明void Log(const char* message)即可,在对整个工程构建(build编译所有文件)时,链接器会找到Log函数的定义在哪里,将函数定义导入到Log函数中。
3、cpp文件也叫做translation unity(编译单元),每个cpp都会被编译器编译成一个obj。当一个cpp文件中include多个cpp时,依旧只有一个translation unity(编译单元),对应生成一个obj。这就是translation unity(编译单元)和cpp文件术语上区别的原因。
4、c++中文件只是存放代码的一种方式,只需要告诉编译器这个文件的类型和编译器该如何处理它
5、编译器在preprocessing预编译阶段检查并评估所有的pre-processing(预编译)语句,常见的pre-processing(预编译)语句:include,define,#if和#ifdef。任何一个以#号开头的语句都被成为预处理命令,或叫预处理指令
6、define的作用是搜索这个词然后把他替换成后面的东西。
7、obj文件是二进制码
8、编译器优化,在生成的obj文件中,去掉一些不必要(重复的)汇编代码执行部分
(但在嵌入式中,这是噩梦,所以要用Volatile来定义一些变量,防止优化)
9、在编译时,就可完成任何常量的计算,不必等到运行时。constant folding(常量折叠)
10、编译:根据源代码文件(source file)生成包含机器码和我们定义的常量数据的二进制文件(object file)。有了object文件后,就可以把这些文件链接到一个包含所有我们最终要执行的机器码的可执行文件。
11、编译过程(产出obj):(1)预处理代码,预处理语句评估(作用)(2)进入标记解释(tokenizing)和parsing(解析阶段),将“英文”C++文本处理成编译器能懂和处理的语言。(3)结果就是创建某种叫做abstract syntax tree(抽象语法树),也就是我们代码的表达,但是是以抽象语法树的形式。(4)创建抽象语法树后,就开始产生代码,即真正的cpu会执行的机器码。
12、C开头-编译阶段错误,LNK开头-链接阶段错误
13、程序入口点不一定是叫做main的函数(默认是main),主要看在编译器中如何设置
14、Link只会链接调用到的函数,没有用到的函数不会去链接它。但若是在主函数所在文件中定义的函数,虽未被调用,但也会链接它,因为这个函数可能在别的文件被调用到
15、当项目中存在多个同名的函数(特别当在头文件中定义时),链接器会报错,因为它不知道该链接哪个。解决方式共有三种:
(1)可以在函数声明前加上staitc,即表示该函数只在该文件内有效。
(2)使用inline,即当这个函数被调用时,直接将函数体的内容拿过来取代调用。
(3)不在include.h中定义函数,而是放在同名的include.c中,.h中只含有该函数的声明,这样子头文件在多次被包含时,就不会被编译器用以加入整个函数体,而是只在该位置加入函数的声明,避免了多个文件中重复定义函数。
16、当我们在头文件中放置函数声明时,调用该头文件,只会在调用文件处加入该函数的声明,并在链接阶段,让链接器自己去工程文件中找到这个函数的定义来使用它。
17、函数声明的作用!:
(1) 当函数定义不在该cpp文件时,告诉它,有这个函数存在,只是它定义于别的地方。
(2) 告诉所在文件,有这个函数的定义
(3) 函数的声明实质就是函数定义的第一行。只有知道函数声明,也才能知道如何去调用它
18、头文件:
(1)省去了每次复制粘贴函数声明的工作,只需要在头文件中写函数声明。
(2) 在cpp被编译时,声明会被自动复制粘贴到include头文件的位置。
(3)头文件(如Log.h)里声明的函数,不用是和它相同文件名cpp里定义的函数,任何函数(如void InitLog())声明都可以,与文件名没有任何关系。
(4)包含头文件的意思,也只是把头文件中的内容复制粘贴到包含的位置。
(5)头文件名和cpp名相同,通常包含的函数也相同,只是因为这样子看起来架构更合理,更清晰一点而已。
(6)函数的定义:在链接阶段,调用这个函数的(目标)文件会来链接它,无论它写在哪个cpp文件中。
19、头文件保护符
(1)#pragma once:新的保护符,只用写一行。
(2)
#ifndef_LOG_H
#define_LOG_H …
#endif
老保护符,较为麻烦。
20、头文件包含方式
(1)<>从include目录中去找(系统包含的标准库)
(2)””从当前相对目录(自己写的文件)和从include目录去找,相当于可以包含所有文件。
21、c标准库和c++标准库的区别:c标准库头文件有.h,c++直接是名字。
22、一个程序实际上是由内存组成的。
23、数据操作符实际上都是在标准库等有人写好的函数,我们直接调用。大部分的原始数据结构,实际上在检查两个整数是否相等时,是逐个字节逐个位进行比较的,在内存中对应位的二进制数都得相同。
24、VS中debug运行状态,右键选择go to disassembly(ctrl+G),可显示源代码及其对应的汇编代码。但还是尽量避免debug汇编语言,用来查询错误太难。但用来快速查看编译器产生了什么样的代码,还是挺有用的
25、C++变量
(1)不同数据类型间,真正的唯一区别是大小(分配多少内存)
(2)unsigned(无符号数),用于去除变量类型的符号位(1位)。unsigned int
(3)5种最基本的数据类型
类型 | char | short | int | long | long long |
所占字节数 | 1字节 | 2字节 | 4字节 | 4(看编译器) | 8字节 |
(4)定义float变量时,数值后面要+f,否则会被当成double。float variable = 5.5f;
(5)float4字节,double8字节
(6)bool变量只能为"true"和"false"实际值为1和0。除了0以外的任何整数都是1。
(7)bool变量占用一个字节byte,但实际上只需要一个位bit,但由于计算机中的寻址是按字节byte来的,所以需要占用一个字节来找到它,我们也可以将8个bool存放在1个byte里。
25、if语句
(1)else if实际上就是被写在了同一行而已
else
{
if()
}
实际上和else if一样的,其实就是个else和if。C++定义中也没有else if这样的关键字,仅仅是else和if
(2)if语句中通过比较条件,跳转到内存的其他部分执行指令,如果语句和分支分布在内存距离较远的地方,需要的时间就长。高效代码会尽量减少if语句。
26、编程有两重要的组成部分(1)数学编程(2)逻辑编程
if语句、条件语句、循环、控制流语句是可以用来修改程序运行情况的唯一工具。
27、控制流语句:
(1)continue:跳过本次迭代,直接进入下一次
(2)break:直接跳出整个循环
(3)return:完全退出函数。return可写在任何地方
28、内存是计算机的一切,编程中唯一最重要的东西,可能就是内存(memory)。当你写一个应用程序并启动它时,整个的程序被载入内存,在你写的代码中,所有的指令告诉计算机要做什么,所有的这些都被加载到内存中,这就是CPU如何访问你的程序并执行指令的。
29、指针:
(1)所有类型的指针都是一个整数,存放一个地址
(2)指针本身没有类型,类型只是说它存放的地址中的数据,可能是这个类型
(3)给指针初值0(也就是NULL),实际只是意味着这个指针无效,0是无效的,内存中没有0这个内存地址。
(4)但如果指针类型是void,在对它指向内存操作(赋值)时,会报错,因为不知道这块数据是short两个字节,还是int四个字节,就无法完成操作。
(5)指针本身也是变量
(6)指针的大小根据操作系统位数,看它的内存地址是多少位的,一般是32位4个字节
30、引用:
(1)引用实际上只是指针的一个扩展,本质上引用就是指针。
(2)引用只是基于指针的一种语法糖,使得代码更易读易写。
(3)引用不像指针要先创建一个指针变量,“引用”必须引用一个已存在的变量,其本身并不是一个新变量。它只是其他变量的引用。引用必须要有初值。
(4)int&中&是类型的一部分,表示引用。
(5)int& ref = a;ref并不是真的变量,只是在代码中当成一个变量使用,在编译时,也不会创建一个ref的变量。但可以将ref就当成a来使用。ref就是a的别名。
(6)&引用就是变量本身,不是它的地址。ref就是a
(7)引用一旦声明,就无法改变它所指向的对象,即ref已经是a的变量,就不能再变成b的别名。
31、类:
(1)一种将数据和函数组织在一起的方式。
(2)类名必须唯一,类基本上就是创建了一个新的变量类型。
(3)由类类型制成的变量叫做对象,新创建对象的过程叫做实例化。
(4)默认情况下,类中成员的访问控制都是私有的,只有类内部函数才能访问这些变量。
(5)类内的函数称作method(方法)
(6)struct和class的唯一区别是,struct默认是公有的(public)
(7)当结构体使用继承时,会收到警告,但依旧可以运行,但是用类就不会有这样的问题。
(8)当只是包含一些变量结构时,通常采用结构体好些。当包含大量功能时,用类好些。
(9)在技术上说,可以在任何使用class的地方使用struct,就是可见性上的区别
(9)构造函数会在你每次实例化类的时候,自动调用
(9)可以有很多同名,但不同参数的构造函数(函数重载)
(10)创建对象时不会自动给变量赋初值,构造函数默认也是空函数,必须要手动赋初值,否则就是之前留在内存中的值,可能也无法用公共的std::count函数打印。
(11)可以用delete告诉编译器,我不想要那个默认构造函数Log() = delete;,该类就无法创建对象,而只能调用其中方法。也可以用private来隐藏构造函数
32、类的写法
(10)成员变量前加上m_进行标注,以便和局部变量(用于函数等)区分开。
(11)写法习惯:有两个public,public方法放在一部分,public变量放在另一部分。同时public static变量放在另一部分。
33、类中的static
(1)static在类中有两种:在类class或结构体struct外使用static,或在类内和结构体内使用static
(2)类外的static修饰的符号(定义的函数和变量)在link(链接)阶段是局部的,只对定义它的编译单元(.obj)可见。类内的static表示这部分内存是该类所有实例共享的,即实例化很多次这个类,但这个静态(static)变量只会有一个实例。
(3)静态方法同样,只有一个,静态方法中没有该实例的指针(this)
(4)静态方法不能访问非静态变量,静态方法没有类实例,没有获得当前的类实例作为指针(this指针)
(5)函数中的静态变量,生命周期是整个程序,作用域是函数内.
(6)使用静态类方法,可以直接创建一个类的实例。
(7)两个全局变量即使在不同的文件中,也不能同名,在link(链接)时,会同时检测到两个同名全局变量,重复定义。
(8)extern相当于引用,当变量前加了extern就会去其他文件找到已经定义的这个同名变量,成为它的引用。
34、枚举值设置:
enum Example
{ A, B, C};
(1)创建的这个枚举类型叫Example,用Example类型定义变量时,它的值也被限定在了这三个值之一。
(2)默认从0开始,即A为0,B为1,C为2
(3)也可赋值任意整数。A=5,此时B、C自动顺延为6、7。
(4)枚举值默认类型是32位整数,可以设置枚举值的数据类型。例如8位整数:
enum Example : unsigned char
(5)数据类型只能是整形,不能是像float,但可以是char(进一步说明char是种整形数据类型)
35、构造函数:
(1)构造函数会在你每次实例化类的时候,自动调用
(2)可以有很多同名,但不同参数的构造函数(函数重载)
(3)创建对象时不会自动给变量赋初值,构造函数默认也是空函数,必须要手动赋初值,否则就是之前留在内存中的值,可能也无法用公共的std::count函数打印。
(4)可以用delete告诉编译器,我不想要那个默认构造函数Log() = delete;,该类就无法创建对象,而只能调用其中方法。也可以用private来隐藏构造函数。
36、析构函数:
(1)当一个对象被销毁时,析构函数都会被调用.
(2)可用于栈(直接创建)和堆(new创建)。
(3)析构和构造唯一区别,函数前的波形符(~)。~Entity()
(4)调用类的函数结束后,即}符号后,会自动调用析构函数,销毁对象,避免内存泄漏。
37、继承
class Player : public Entity
(1)把一系列类的所有通用代码(功能)放到基类(父类)中,这样就很容易从基类中创建派生类。避免重复代码
(2)父类中任何不是私有的(private)成员,子类都可以访问
(3)继承只能继承公有public的部分
(4)Player不仅仅是Player类型,还是一个Entity类型。Player也是一个Entity。
(5)可用在任何想用Entity(父类)的地方使用Player(子类)。子类总是父类的一个超集。
(6)继承是用来扩展类和为基类提供新功能的一种方式。
38、虚函数
B是A的子类
(1)在A中新建一个方法标记为虚函数,那么就可以在B类中重写这个方法让去做其他事情。
(2)用虚函数的原因:在类中正常声明函数或方法,当调用该方法时,总是会调用属于这个类型的方法。然而我们实际上是想调用它子类类型中的方法
(3)但希望让C++能意识到,我们传入的其实是一个Player,所以请调用Player的GetName,这就要用到虚函数
(4)虚函数引入了一种要动态分派的东西,一般通过虚表(vtable)来实现,虚表即一个包含类中所有虚函数映射的列表。通过虚表可以在运行时找到正确被重写的函数
(5)虚函数:父类函数名前面加上virtual,子类函数名后加上override
(6)虚函数的额外成本:
a、需要额外的内存存储虚表,基类内还有一个指针成员指向虚表
b、每次调用虚函数时,需要遍历虚表找到最终要运行的函数。
39、纯虚函数:
(1)允许定义一个在基类中没有实现的函数,强制子类去实现这个函数
(2)基类(父类)中缺乏定义,直接=0(相当于没有定义)
(3)纯虚函数必须在子类中实现,才能创建这个类的实例。
(4)有纯虚函数的类,称为接口
(5)可将虚函数的类作为参数传入通用函数,即可调用这个函数(完成多个类同一功能的输出)
40、可见性
(1)C++中三个基础的可见性修饰符:private和protected、public。
(2)protected比private更可见,比public更不可见。即所有子类可以访问,但外部不可见。
41、数组
(1)数组基本上就像在一个变量中有多个变量。
(2)如果想打印整个数组,需要打印这个数组的地址(数组名)
(3)数组连续的存储数据,即他们的内存地址是连续的。
(4)访问数组中的特定索引,实际上是通过对内存取偏移量实现的。例如 int example[5]。如若要访问example[2],因为是int四个字节,内存地址总共增加(偏移)2*4=8个字节。
(5)在栈上创建的(正常创建),到达函数最后花括号会被销毁。在堆上创建的(用new),除非程序主动用delete销毁,否则始终处于活动状态。
(6)用new创建数组时用了数组操作符[ ],所以delete后也要加上[ ],即delete[ ]
(7)std::arraya,C++11中带有的数组,有边界检测、记录数组大小等功能,但也意味着额外的开销。
(8)永远都不要在数组内存中访问数组的大小,但可以用sizeof()得到这个数组占了多少字节,然后除以它的数据类型的大小,得到数组元素个数(大小)。但sizeof通常不能用于堆上创建的数组,因为要访问它,通常要通过一个指针,sizeof得到的也只是那个指针的大小(4字节)。
(9)但使用sizeof计算数组大小也有危险,当数组在函数(可能已经销毁)或者变成了指针,会出错
(10)在栈中创建数组时,数组大小这个值必须在编译时就要知道的常量。(C++特性)
static const int exampleSize = 5;
(11)类中的常量表达式必须是静态
42、字符串:
(1)字符就像是字母、符号和数字以不同的形式展示出来
(2)C++中char字符类型占一个字节,默认处理字符方式是ASCII字符(256个刚好一个字节数量)
(3)字符类型可以大于一个字节,当要使用到例如中文时,一个字节(256种可能)明显放不下,就可能要扩展为两个、三个、四个字节的字符。当要把它和英文字母、数字、符号放在一起时肯定远远多于256
(4)字符串是一个固定分配的内存块,不可能让它变得更长,通常创建时直接用const
(5)C++中用双引号定义字符串时,它其实就是const char数组,使用std::string 时要加上const
(5)C++中的字符是用单引号,不是双引号,双引号默认是char指针。
char* name = “Cherno”;
char name2[7] = { ‘C’, ‘h’ , ‘e’ , ‘r’ , ‘n’ , ‘o’ , ‘/0’ }
(6)C++标准库有个类叫string,还有个模板类basic_string。
(7)std::string本质上是这个basic_string的char作为模板参数的模板类实例,即模板特化(template specialization)。就是把char作为模板类basic_string的模板参数,意味着char就是每个字符背后的数据类型。
(8)若要打印处字符串,需要再引入string库,即使iostream库中已经包含string定义。因为在string头文件中这个操作符的被重载实现了允许我们把字符串传入输出流
43、字符串字面值:就是在双引号之间的字符“cherno”
(1)字符串字面值是存储在内存的只读部分的,无法通过指针只想改变它的值。只能把它重新赋值给一个新建数组,再对这个数据进行修改
(2)C++11后,用指针指向字符串前必须要加上const,本身字符串字面值也是不可改变,const相当于使用时限制了这一操作,防止出错。
(3)字符串前写上字母R,意味着它会忽略转移符
44、const
(1)承诺某些东西不会改动,但对实质性内容没有修改。这个“承诺”可以绕过。如:强制类型转换 a = (int*)&MAX_AGE
(2)使用const后,当你还尝试去改动时,编译器会提醒报错。
(3)指针:
a、const在类型前:不能改变指针指向的内存地址的内容。const int* a = new int;
b、const在类型后:不能改变指针指向的内存地址。 int* const a = new int;
c、const后跟*:和在类型作用前一样。int const* a = new int;
d、结合a和b:既不能改变指向,指向的内容也不能变。const int * const a = new int;
(4)类:
a、对类的方法:表示该方法只能读取,不会修改类中数据(包括成员变量)。
b、在方法的声明后加上const
(5)引用:
a、加上const后不能做任何修改,因为引用就是那个对象
b、方法定义时没用const,而调用时用了 const类,则无法用方法
(6)mutable关键字:定义变量后,即使有const也可以修改。
45、mutable关键字:有两种用法
(1)和const一起使用
(2)用在lambda表达式里(函数声明后),或同时也和const一起使用。
auto f = [=]() mutable
(3)lambda类似个一次性的小函数,可以将它赋值给一个变量。
46、栈的空间通常较小,堆的较大,栈中的变量生命周期在声明它的地方就指定了,堆中创建的变量(内存)需要手动删除才能释放
47、(1)用new创建对象时,要用ListNode* dummyHead = new ListNode(0); 要加上*,因为new分配内存后的返回值是该类型的指针(该块内存的首地址)
(2)调用new时候,会调用C中的malloc(sizeof(Entity))分配内存,但是比起malloc,new会调用构造函数。
48、explicit在构造函数前声明后,构造函数就不能隐式的进行类型转化,只能显式的进行
49、C++中运算符就是函数,例如add这样的操作可以交给+运算符。
50、浅拷贝:没有实际复制一个新的存储空间,相当于只是拷贝了指针,指向相同的内存。
深拷贝(定义了拷贝构造函数):在复制同样的对象时,开辟了新的存储空间,相当于拷贝了一个新的对象。
51、箭头运算符:当指针指向一个对象时,搭配->可以访问该对象的成员和方法。:
Entity* ptr = &e; ptr->Print(); (Print是e中的方法)
52、标准库也被称为标准模板库,标准模板库本质上是一个库,装满了容器,容器类型,这些容器可以包含特定的数据,可以模板化任何东西。
53、vector本质上是一个动态数组ArrayList,大小长度可变。当添加新的元素的时候,会寻找一个更大的存储空间,将现有的元素复制过去。
vertices.push_back():添加元素
vertices.erase():删除元素
Array会在栈上创建,而vector会把它的底层存储在堆上,技术上看,返回std::array会更快
std::vector是C++标准模板库中的类
54、dll是运行时动态链接库,在运行时动态链接。
lib是静态库,静态链接是在编译时发生的
55、tuple(元组):一个类,可以包含x个变量,但它不关心类型(类型可以不同)
56、(1)模板就是让编译器帮你写代码;
(2)模板并不是实际存在的,只有被调用时,才会被创建。
(3)调用时等价于复制这个函数,然后把类型自动替换为我们需要的
57、
栈分配:有栈指针,当遇到变量创建时,反着移动需要的空间字节位置,空出内存进行分配,返回栈指针的地址。当作用域结束时,直接弹出,栈指针回到作用域之前的位置。(多个不同变量时,也是连续的存储空间,只是中间添加安全守卫(一些数据cccc))
堆分配:堆用new分配时,(实际调用了malloc)从“空闲列表中”寻找符合空间大小,空闲的内存空间。
堆上分配内存是一堆的事,栈上就只像一条cpu指令
58、auto具有两面性:(1)当更换类型时,不用太多的修改 (2)但当原先类型调用了它单独有的方法时,可能会报错,比如string类型的name.size()
59、静态数组:不增长的数组。std::array<int, 5> data;
相比普通的 int data[5];静态数组是个类,存储了它的大小可以直接访问:data.size()
60、lambda:匿名函数,相当于用完即弃的函数,在代码过程中生成。
61、类本身也是个命名空间
62、多维数组其实就是数组的集合,二维数组是数组的数组,三维数组是数组的数组的数组
二维数组:每个元素包含了50个int型的数组,元素是指向该数组的指针
三维数组:在外层又套了层指针的数组,每个指针指向一个数组的数组
63、指针就是一个整数,和指针类型(类似int型指针)无关。
64、排序:std::sort(values.begin(), values.end());若不加第三个参数则默认升序排序。
第三个参数可以是自己创建的结构体内的函数,也可以是一个lambda,也可以是内置函数。如std::sort(values.begin(), values.end(), std::greater<int>()); 从大到小排序
65、类型双关:拥有的一段内存,当作不同类型的内存对待。将该类型作为指针,然后转换为另一个指针,如果有必要,还可以进行解引用。
66、虚析构函数:不是覆写析构函数,而是加上一个析构函数
只要些一个要扩展的类或者子类时,(允许一个类拥有子类),必须要声明你的析构函数是虚函数
67、左值是有某种存储支持的变量,右值是临时值;左值引用仅仅接收左值,除非是用const,右值引用仅仅接受右值
68、参数计算顺序:
int value = 0;
PrintSum(value++, value++); C++中对不同参数是并行计算的,所以值可能是0,0
C++17以后要求必须是有顺序的,但是具体顺序仍然是未定义的,也就是值可能是0,1或1,0
69、C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符。