TheCherno C++系列笔记(第一部分)

一、

①iostream等各种库头文件,存的都只是各种的变量&函数的声明。而既然存的只是各种声明却没有实现的话,include这样的头文件进来之后是通过库文件来使得我们可以直接去使用它里面包含的函数&对象的--可以理解为include进来对应的库头文件之后,库文件(一般是在MSVC文件夹下,.lib后缀)也会自动在这个项目工程里加载。

②main函数永远是程序的入口,也可以在VS里设置别的自定义函数入口,但别这么干。main不需要显示的写明返回值,程序正常执行完毕后会返回0,而有时候程序异常的时候终端也会显示程序的结果为-1

③要注意"<<"">>"本身也是作了运算符的重载,从形式上就可以知道它们的operator重载函数是i/ostream的成员,流也是读到对应的对象里最后再flush到终端(控制台)

④当需要手动打开某些.exe文件来测试等时,注意要在程序的末尾添加cin.get&getchar()&getch()或者是while(1){}这样的空循环等,以防秒关--不这样做的话只有在编译器里生成程序时才不会立刻关掉你的控制台窗口!

⑤Debug mode会慢于Release mode不少,因为会额外生成许多的文件,但也正是这些多余文件的生成会利于Debug。

int a()
{
    return 0;
}
int main()
{
    a();
}

以这段代码为例,如果我们在调试时选择调试-窗口-反汇编,debug模式下会发现编译器生成了函数a相关的汇编码,而release模式下就不会,因为release模式需要的是速度,编译器发现函数a没有任何用处时就会跳过那段代码。但也正是Debug mode下的这些“多余”的码易于我们Debug

⑥VS2019的一些快捷键:ctrl+K+C--一键注释;ctrl+K+U--一键取消注释;ctrl+K+D--把代码的排列变成好康的样子;单下Tab--补齐代码;两下Tab--代码联想--最常用的就是for/while/if等语句(有时候只敲一下Tab);ctrl+F7--生成项目(单独编译一下);ctrl+enter--在当前行生成空行(这个很爽!);ctrl+shift+U--转换为大写;ctrl+U--转换为小写

⑦单独去跑每个项目的时候会生成.obj文件,但是如果去右键项目名去build整个项目的话就会生成.exe文件

⑧对于链接器--从编译器的工作原理来看--包含编译+链接,编译器做的事情是把.cpp文件转换成.obj文件,而链接时,各个.obj文件会传入Linker,然后由Linker来把各个源文件link到一起。

因此在其他的.cpp文件中定义的函数,在另一.cpp里面只写声明就可以,因为最终会link到一起--Linker也相当于是负责了链接声明&定义!

所以可以认为Cpp里没有文件这一说--所有的所谓的“文件”,都只不过是给编译器提供源码,用于生成对应的.obj文件的工具,也就是一个"translation unit"。即使你写了个后缀名叫lebronJames的文件,只要告诉编译器以Cpp的方式编译,它照样也可以生成对应的.obj文件。所以“文件”代表不了任何东西。

⑨要注意.cpp文件,编译器默认会以Cpp的方式编译,而.c文件编译器会默认以C的方式编译。因此书写C程序的时候最好把源文件的后缀名都改成.c。

⑩但是!项目源文件和translation unit还是有区别的——一个项目可以有很多个源文件,单独去编译每个源文件只会得到对应的.obj,而build整个项目就会生成对应的.exe文件

二、

①include的作用相当于就是打开对应的文件,找到你想要include的内容,然后再粘贴进去!

可以在对应的项目的属性处把这个选择为是来看预处理之后的结果--.i文件

②#ifdef/#endif的好活用法:说实话其实并不如#if DEBUG_MODE==1  #endif这种的写法,因为前者在修改的时候需要去删除或者添加宏定义,这是我们不希望的。项目中写代码最好的做法就是要么只需要增添代码而不动源码,要么就是能够对代码进行极小的调整(其实也不如只添加,只要涉及了修改就不大好)。而后者的写法只需要修改一下对应的宏定义的值即可。

但是!有几个适用场景是很舒服的--VS在一些模式下会有一些特殊的宏定义,

1.譬如说debug模式下会有_DEBUG的定义,我们debug模式的测试代码就可以放在#ifdef _DEBUG和#endif之间(release模式是没有_RELEASE这种的宏定义的,所以只去依照_DEBUG来判断处于什么模式即可!)

比如下面这段代码,在debug模式下ctrl+F7编译就可以在输出窗口看到message内的内容

#ifdef _DEBUG
#pragma message("DEBUG_MODE ACTIVED!")
#endif

2.如果定义了_WIN64表示x64版本,否则是x86版本。同样是依据一个宏有没有定义来判断是多少位的操作系统,因为在Win32配置下,_WIN32有定义,_WIN64无定义,在x86配置下,_WIN32和_WIN64都有定义。

同样的,既然_WIN32在任何windows操作系统中都有定义,也可以通过_WIN32是否被定义来看当前的操作系统环境是否是windows操作系统

③编译&链接时的一些特殊工作:

首先是编译时的常量折叠问题,任何的常量都会在编译时进行计算,比如#define,比如const,比如constexpr。当然,模板的实例化也会在编译时进行。

比较新鲜的东西:constexpr。constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。既然是能够在编译阶段通过“静态法”得到结果,那么constexpr&const声明过的变量是可以用于静态数组定义时用于指明数组长度的变量的!

同样的,constexpr可以用来声明函数的返回值是一个常量--这样完全可以通过编译

constexpr int display(int x) 
{
    int m = 1 + 2 + x;
    return m;
}
int main()
{
    int arr[display(1)];
}

但是这样是不可以的:

int a = 0;
constexpr int display(int x) 
{
    int m = 1 + 2 + x;
    return m+a;
}
int main()
{
    int arr[display(1)];
}

很明显,display()函数自然是有机会拥有一个非常量的返回值的,声明静态数组长度的时候自然会报错。

同时,constexpr是不可以修饰自定义类型的(比如struct&class),如果想要表示成员是常量,可以在构造函数处进行constexpr声明:

class A
{
private:
	int a, b;
public:
	constexpr A(int c,int d):a(c),b(d){}
};
int main()
{
	A a(1, 2);
}

※以下摘自C语言中文网:C++11 constexpr:验证是否为常量表达式(长篇神文)

注意,constexpr 修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值(不用初始化列表的话,就无法初始化,因为函数内部进行的是赋值操作)时,必须使用常量表达式。

前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4 个条件。

constexpr修饰模板函数

C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。

针对这种情况下,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。

举个例子:

#include <iostream>
using namespace std;

//自定义类型的定义
struct myType {
    const char* name;
    int age;
    //其它结构体成员
};
//模板函数
template<typename T>
constexpr T dispaly(T t){
    return t;
}

int main()
{
    struct myType stu{"zhangsan",10};
    //普通函数
    struct myType ret = dispaly(stu);
    cout << ret.name << " " << ret.age << endl;
    //常量表达式函数
    constexpr int ret1 = dispaly(10);
    cout << ret1 << endl;
    return 0;
}

程序执行结果为:

zhangsan 10
10

可以看到,示例程序中定义了一个模板函数 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

  • 第 20 行代码处,当模板函数中以自定义结构体 myType 类型进行实例化时,由于该结构体中没有定义常量表达式构造函数,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;
  • 第 23 行代码处,模板函数的类型 T 为 int 类型,实例化后的函数符合常量表达式函数的要求,所以该函数的返回值就是一个常量表达式。

摘录完辣!写的太好了

再一个,汇编指令中会有一个叫做函数签名的东西,这个函数的名字的周围会有一些的#和@等,主要是因为一个函数在位于多个.obj中时,会在多个.obj文件中被定义,所以需要函数签名来对不同的.obj中的函数作区分。

链接时:Linker找到每个符号&函数的位置,链接起他们来,譬如把声明&定义链接起来,譬如链接起来main(程序入口)和其他的成分,把项目文件从源码变为可执行的二进制文件。而且每个.obj都是相对独立的,不同的.obj文件之间是无法沟通的,所以项目中有多个.cpp文件生成多个.obj文件时,就需要Linker把它们链接起来。即使只有一个文件也要链接main和其他部分。

由此,引出的Debug时的一些信息:以C(complie)开头的报错--编译阶段(ctrl+F7)错误,往往就是无聊的语法错误或者一些偏点;以LNK(Link)开头的报错--链接阶段的报错,尤其是函数定义找不到等,这种的函数定义找不到的错误往往一处会有两条报错信息,重点观察报错信息中的函数名来定位到具体函数看看定义处函数名是否写错等即可--而且往往编译器会告诉我们我们在哪里“引用了这个符号(指函数名)”,实际上就是告诉我们在哪里调用了。但是注意即使声明了但是没有函数体的那个函数没有被调用过,也会报错,因为我们仍然是“可能”会在其他文件中调用此函数的,所以Linker依然会去尝试找到函数的定义来和函数声明做链接!找不到了,自然就报错了。然而这种东西也是有解决方案的--可以告诉编译器我们只在这个文件使用这个函数,然后在这个文件中只声明而不定义,也不调用它,这样编译器才会认为这段代码没用而放心的移除它,不报错。(就是给函数一个static声明而已,声明它只会在这个编译单元中生效,使得对此函数链接时,让他明白链接只应当发生在该文件内部。static相当于一个相对于整个项目的“private”声明符)

④预处理相关信息&重定义问题的一些解决方略:

对于预处理,任何以#开头的东西都被称为这个程序的预处理指令,譬如#pragma,本质上是一个被发送的预处理指令,注意预处理是在编译之前进行的——比如#pragma once这条指令,会防止我们在同一个编译单元之中多次包含含有此条指令的头文件,因为由于头文件的复制粘贴的特性,重复包含自然会引发各种的重定义问题。这里的重复包含往往不是直接包含引起的,往往是被重复包含的这个头文件存在于其他头文件中,然后又包含了其他的头文件而间接的造成重复包含的问题。当然,在多个.cpp文件中是可以的,这里一定不要混淆!

一个引例--math&log.cpp文件中都定义了一个函数,然后log.h中有这个函数的声明,并且这俩.cpp文件都include了这个头文件--这里也是代码“二义性”的一种表现。(补充一点,VS中的各个文件夹只是文件夹而已,并不会限制含有的文件的类型。所以你往源文件夹里面新建头文件也没人拦着你,只是这样很弱智)此时有三种方法来解决:

第一种,最垃圾的一个,太容易造成混淆了,千万别用--在头文件处加上static使得这两个.cpp文件分别在自己的翻译单元中有自己的版本的log函数,也就是每个编译单元内的这个函数对于其他的.obj 文件都是不可见的。

第二种,这个还比较可行,进行内联函数的处理,也就是在函数名前面加上inline关键字(⑤会有内联函数的具体介绍)

第三种,最常用的,在头文件中放函数声明,只在其中一个.cpp文件中定义具体的函数体。

⑤内联函数inline:

inline的意思实际上是获取函数体,然后把函数体置于调用函数的地方。但注意inline函数也是不可以重复定义的!

inline的主要作用:当函数体并不复杂时,进出函数的开销很可能会比函数体本身做工作要耗费的性能大,就像跑到楼下小卖部只为了买一瓶水一样。

下段摘自C语言中文网:C++ inline内联函数详解

函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。

因此对于一个比大小的函数,为了改进这种情况,可以采用宏定义的方式来书写:

#define GETMAX(a,b) ((a)>(b)?(a):(b))

但是这样写会有一定的运算符计算的问题,譬如后置运算符一般的在一行内的赋值运算中是不会++的,比如:

int a=0;
int m=(a++)*(a++)+(a++);

此时a=0这个条件在int m所在的这一行都是生效的,所以m最终的值为0。

但上面的宏定义那里就不一样了--比如:

	int a = 1;
	int m = GETMAX(a++, 0);
	m = GETMAX(a++, 0);

最终得到的结果是a=5,m=4;

计算过程可以理解为是a在GETMAX这里进行函数传参的时候相当于是:

((n++)>0?(n++):0);

先是左侧进行比较大小的这个运算之后a+=1变为2,再把2赋值给m。此时a再++变为3。然后下一次GETMAX调用时仍然是比较完后++再进行m的赋值操作,然后再++,就会得到这样的结果。

但是很明显,这种结果肯定不是我们想 要的,我们理想中的应该是m第一次被赋1,第二次被赋2,且最终a=3。

由此引出了既可以避免这种情况又可以降低调用开销的内联函数--只需要在函数名前面添加一个inline关键字。但注意一般是多次调用这个函数的时候才有必要去把它写成内联的形式,而且我们只会在头文件里写成内联(为了方便其他的.cpp文件include进来并调用)——但是内联是一个“建议”,你建议编译器写成内联形式而已,编译器不一定会遵从,譬如Debug模式下编译器就不会给你写成这种形式,为了方便调试。

下段摘自C语言中文网:C++ inline内联函数详解

但是要注意,要在函数定义处添加 inline 关键字,在函数声明处添加 inline 关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的 inline 关键字。使用内联函数的缺点也是非常明显的,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数。

程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的节省。

一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!

另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行)

有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数)。

虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数————但是由于虚函数往往是运行时确定调用它的对象,而inline是编译时作代码的处理,因此:

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

上面三点摘录自:C++ 内联函数 | 菜鸟教程

⑥Cpp中的变量:

1.两个思想:

“每个数据类型都有自己的作用,但我们也可以不用这些作用”

“所有的变量的区别只有大小”————记得没什么特殊需求的话  都写unsigned就完事了。

2.short的存在意义--在32位平台下如windows(32位)中short一般为16位,小于3w2左右的数都可以用short写,unsigned short就能到6w5左右,但是一般不写short。short的大小可以认为是>=2字节。

3.bool型存储优化以及vector<bool>的雷点:出于内存寻址的最低大小是一个字节,因此bool型变量即使只需要一个bit,也得用一个字节来存-----但是,计算机是可以实现8个bool型变量存到一个字节中的!(但是我们仍然需要那个字节来完成寻址的操作),vector<bool>就实现了这样的操作:

可以参考C语言中文网的概述:

​​​​​​vector<bool>不是存储bool类型元素的vector容器!

关于文章中提到的bitset,可以参考这篇:

[C++] 用bitset代替bool数组的性能测试以及bitset的基本使用_C+G的博客-CSDN博客_bitset空间复杂度

关于Cpp中为什么有bool类型的文章:

C++中为什么使用bool类型_lee_鹿游原的博客-CSDN博客

其中有一段非常重要:

如果你用的编译器支持运算符!的别名not,而你也想提高可读性则可以使用not替换!运算符,写作if( not true),而且鼓励这样写,如代码1-5。not是C++标准的关键词,但如果您的代码要求可移植性比较高,还是不要使用该关键词,因为有些编译器可能不支持该特性。如果您还是喜欢用not,同可以使用#define预处理指令定义为宏(宏本质上就是个复制粘贴,所以可以替换任何符号!)。当然对于&&和||、^您都可以使用其别名:and、or、xor

其他的讨论:

c++ - Why isn't vector<bool> a STL container? - Stack Overflow

因此替换方案大致是std::bitset&std::deque&std::array,但也不是不能用vector<bool>,虽然STL本身都不建议使用它。下面是使用vector<bool>的一个实战案例:

c++中bool数组与bitset,vector<bool>的使用与占用空间大小对​​​​​​比_阳光可乐的博客-CSDN博客_bool数组 c++
对于std::bitset的使用方法的概述:

C++——std::Bitset_zy2317878的博客-CSDN博客_std::bitset实现

注意一下这里:

bitset< n > b(s, pos, n);根据string s中创建pos位置开始n个元素的bitset b

这里,s的下标从0开始,而这种声明方式相当于是从s的下标为pos的位置起,取n位,塞到这个bitset的末尾。

并且,bitset中,最右边的元素是第一个元素,亦即下标为0的元素!

4.关于signed和unsigned char的问题:

char与signed char, unsigned char的区别_zx824的博客-CSDN博客_char和signed char的区别

总之,VS默认的是signed char,如果声明为unsigned的话,会有一些不必要的麻烦,建议一般就写char--不过注意一点,基本的ASCII码是0-127,signed char正好够用,而128-255还有128位的扩展ASCII码,如果对这些ASCII码有特殊需求的话,也许真的需要使用unsigned char。

注意,arm-linux-gcc把char定义为 unsigned char,进行编译使用时要格外注意。

ASCII码查表:

A​​​​​​SCII码 - 基本ASCII码和扩展ASCII码,最全的ASCII码对照表

5.对于各种前置知识的继续补充:

(1).STL模板库中的内存分配器Allocator:

C++ 标准模板库(STL)——空间分配器(allocator)_JMW1407的博客-CSDN博客

(2).noexcept 关键字 - 知乎

本质上noexcept就是一个防止异常扩散的关键字--

比如这样写,编译器就会抛出警告:

 

 (3).一个有意思的小问题:

c++ - uint8_t can't be printed with cout - Stack Overflow

uint8_t就是个8位的数据类型,实际上是一个unsigned char,所以cout值小于32的uint8_t的类型的东西的时候是无法打印的!因为ASCII中0-31是non-printable的。

(4).Cpp20的新特性:std::format,可以不急着看:

formatting - how do I print an unsigned char as hex in c++ using ostream? - Stack Overflow

(5).逐位计算的典型案例:(这里面对于Union的使用的妙处?)

byte - C - unsigned int to unsigned char array conversion - Stack Overflow

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值