C++基础知识(2)

2021/9/6 22:08:01

本文主要是介绍C++基础知识(2),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一 C++内存分区

二 指针&引用

指针和引用的区别:

(1)指针和引用都是一种内存地址的概念,但是指针是一个实体,引用是一个别名。

(2)指针指向一块内存,内容是所指内存的地址,指针的内容是可以改变的,有const与非const之分,甚至可以为空。sizeof得到的是指针类型的大小。

(3)引用只是一个内存的别名,引用必须且只可以在定义时被绑定到一块内存上,后续不可以更改,也不为空,没有const与非const之分,sizeof得到的是它所代表对象的大小。

(4)在参数传递时,指针必须解除引用才可以对对象进行操作,而直接对引用进行的修改都会作用到引用所指对象上。

三 内存对齐

内存对齐的理由

(1)经过内存对齐之后,CPU 的内存访问速度大大提升。因为 CPU 把内存当成是一块一块的,块的大小可以是 2,4,8,16 个字节。在读取内存的时候是一块一块进行读取。比如说 CPU 要读取一个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进行处理即可。如果数据是从 1 字节开始的,就首先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进入寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4 字节的数据进入寄存器,所以说,当内存没有对齐时,寄存器进行了很多额外的操作,大大降低了 CPU 的性能。

(2)有的 CPU 遇到未进行内存对齐的处理直接拒绝处理,不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。所以内存对齐还有利于平台移植。

在 Linux 中,默认对齐参数为4。

内存对齐的规则

简单来说:数据项只能存储在地址是数据项大小的整数倍的内存位置上。

(1)对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍。

(2)结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍。

C语言和C++中空类和空结构体的大小 。

在C++中规定了空结构体和空类的内存所占大小为1字节,因为c++中规定,任何不同的对象不能拥有相同的内存地址。

而在C语言中,空的结构体在内存中所占大小为0。

四 内联函数与宏定义

宏定义和内联函数的区别

1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。

    内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。

2. 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换

    内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率

3. 宏定义是没有类型检查的,无论对还是错都是直接替换

    内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率

来自 <https://www.cnblogs.com/dynas/p/7015748.html>

使用宏和内联函数都可以节省在函数调用方面所带来的时间和空间开销。二者都采用了空间换时间的方式,在其调用处进行展开:

(1)  在预编译时期,宏定义在调用处执行字符串的原样替换。在编译时期,内联函数在调用处展开,同时进行参数类型检查。

(2)  内联函数首先是函数,可以像调用普通函数一样调用内联函数。而宏定义往往需要添加很多括号防止歧义,编写更加复杂。

(3)  内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了(无法将this指针放在合适位置)。

可以用内联函数完全替代宏。

在编写内联函数时,函数体应该短小而简洁,不应该包含循环等较复杂结构,否则编译器不会将其当作内联函数看待,而是把它决议成为一个静态函数。

有些编译器甚至会优化内联函数,通常为避免一些不必要拷贝和构造,提高工作效率。

频繁的调用内联函数和宏定义容易造成代码膨胀,消耗更大的内存而造成过多的换页操作。

原文链接:

https://blog.csdn.net/ljlstart/article/details/51284906

宏定义:

#define MAX(a,b) (a>=b?a:b)

#define Sq(a) (a)*(a)

五 哈希

哈希表/哈希冲突解决方式

https://www.cnblogs.com/shoufeng/p/10591526.html

一致性哈希:

https://www.cnblogs.com/study-everyday/p/8629100.html

六 Extern C

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

extern是编程语言中的一种属性,它表征了变量、函数等类型的作用域(可见性)属性,是编程语言中的关键字。当进行编译时,该关键字告诉编译器它所声明的函数和变量等可以在本模块或者文件以及其他模块或文件中使用。通常,程序员都只是在“*.h”(头文件)中使用该关键字以限定变量或函数等类型的属性,然后在其他模块或本模块中使用。

extern "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明。

七 C++成员初始化列表

成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?

 1)概念:

在类的构造函数中,不再函数体内对变量赋值,而在参数列表后,跟 一个冒号和初始化列表。

 2)使用初始化列表的必要性:

(1) 如果类中有const 成员或引用类型的成员,由 于const对象和引用类型都只能初始化,不支持赋值,所以这种情况必需使用初 始化列表;

(2)类中含有其它类作为成员,作为成员的类将赋值操作禁止了,也必 需用初始化列表;

 3)使用初始化列表的性能优势:

从上面的必要性可以看出适用更多的情况。另 外,如果类中包含其它类,在进入构造函数时,实际上已经构造了其它类的临时 对象,然后在构造函数中再进行赋值操作,完成完整的构造函数。但如果使用初 始化列表,则可以省去构造临时对象,直接完成初始化工作,效率更高。

 4)注意事项:

(1)构造对象的顺序按照成员属性声明的顺序,而不是成员初始化 列表的顺序;

(2) 静态对象只构造一次;

(3) 所有全局对象在main()函数之前被构造, 且一般按照声明的顺序构造。

参考博文 https://blog.csdn.net/sinat_20265495/article/details/53670644

八 C++接受不定长数组输入

C++编程中,可能会出现输入为不定长的数组数据。

例如:输入为一些整数,整数之间以空格隔开。

本来以为这个可以解决,但是很可惜,输入以空格隔开的几个整数后,再输入换行,还是在等着输入。。。。

首先读入一行字符串,也即将键入的以空格为分隔符的一行字符串数字存入一个string对象中,然后用isstringstream进行分割,分割的整数存在变量istr中,然后循环istr>>a,依次输出。

getline仅以换行作为结束符。

使用的时候注意添加头文件#include <sstream>和配合使用的#include <string>。

九 左值/右值/左值引用/右值引用/移动语义

引用必须在声明的时候被初始化。

左值/右值

按字面意思,通俗地说。以赋值符号 = 为界,= 左边的就是左值,= 右边就是右值。

更深一层,可以将 L-value 的 L, 理解成 Location,表示定位,地址。将 R-value 的 R 理解成 Read,表示读取数据。

左值参数是可以被引用的数据对象,如变量,数组元素,结构成员,引用和解除引用的指针等都是左值。常规变量和const变量都可以视作左值,因为可以通过地址访问它们,但常规变量属于可以修改的左值,而const变量属于不可修改的左值(最初左值可以出现在赋值语句的左边,但是修饰符const的出现使得可以声明这样的标识符,但不可以给它赋值,但可以获取其地址)

非左值包括字面常量(用引号括起的字符换除外)和包含多项的表达式

C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。

右值包括字面常量(C风格字符串除外,它标识地址)/诸如x+y的表达式/返回值的函数(该函数返回的不是引用)。即可以出现在赋值表达式右边但不可以对其应用地址运算符的值。

左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可以获取其地址。

左值引用和右值引用

传统的C++引用就是左值引用,使得标识符关联到左值。

右值引用用&&标识,可关联到右值。主要用于移动语义。可以出现在赋值表达式的右边,但不可以对其应用地址运算符。右值包括字面常量(C风格字符串除外)/诸如x+y的表达式/以及返回值的函数(该函数返回的不是引用)。

移动语义

所谓移动其实是转让了数据的所有权,避免了移动原始数据,而只是修改了记录。

其复制构造函数和移动构造函数

复制构造函数(左值引用)

移动构造函数(右值引用)

复制赋值运算符(左值引用)

移动赋值运算符(右值引用)

强制移动

移动构造函数和移动赋值运算符都引用右值,但也可以使用左值作为参数

使用#include <utility>头文件下的move函数

Move(us1)是右值,上述表达式将调用移动赋值运算符,如果没有定义,就调用复制赋值运算符,如果还没有定义,就不支持上述赋值。

十 Map/unordered_map/set/unordered_set

Map/unordered_map

1)需要引入的头文件不同

map: #include < map >

unordered_map: #include < unordered_map >

2)内部实现机理不同

map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。

unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。哈希表详细介绍

优缺点以及适用处

map

优点:

有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作

红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高

缺点:

 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间

适用处:

对于那些有顺序要求的问题,用map会更高效一些

unordered_map

优点:

因为内部实现了哈希表,因此其查找速度非常的快

缺点:

哈希表的建立比较耗费时间

适用处:

对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map

总结:

内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。

但是unordered_map执行效率要比map高很多

对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的

Set/unordered_set

与map和unordered_map类似

十一 友元函数/转换函数

友元函数(非成员函数)

使得类的非成员函数可以访问类的私有数据。通过让函数成为类的友元(友元函数),可以赋予该函数与类的成员函数相同的访问权限。

大多数运算符都可以通过成员或非成员函数来重载,除了四种特殊的运算符(=,(),[],->),它们只可以通过成员函数来重载。

非成员函数不是由对象来调用的,它使用的所有值(包括对象)都是显式参数。

创建友元函数

如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以使用友元函数来反转操作数的顺序。

常用的友元:重载<<运算符

用于显示类对象的内容。要让ostream对象成为第一个操作数,需要将运算符函数定义为友元;

要使重新定义的运算符可以与其自身拼接,需要将返回类型声明为ostream&

类的自动转换和强制类型转换

只接受一个参数的构造函数可以作为类的自动转换函数,把基本数据类型转换为类类型。它是隐式转换的。但这种隐式转换不一定总是我们需要的,当不需要时,可以使用explicit关键字,即在类的只有一个参数的构造函数前添加关键字explicit,即关闭了隐式类型转换,必须使用强制类型转换。

即只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用了关键字explicit限定了这种构造函数,则它只可以用于显式转换,不可以用于隐式转换。

隐式转换发生的场景

转换函数(C++特殊的运算符函数,是成员函数)

可以把类对象转换为基本数据类型,如把stonewt类型转换为double类型,是一种强制类型转换。

用法:

转换函数也存在和构造转换函数一样的问题,即转换函数的隐式调用会在用户不希望进行转换时发生转换。

这里有2种解决办法:

(1)在转换函数前加explicit关键字,这样必须使用显式调用

(2)用一个功能相同的非转换函数替换该转换函数即可,仅在被显式调用时,才会执行转换。

转换函数总结

十二 成员函数的重载/覆盖/隐藏

成员函数的重载

const成员函数的重载和非const成员函数的重载

https://blog.csdn.net/weixin_37319161/article/details/78744490

成员函数的覆盖

成员函数的隐藏

十三 类的动态内存分配

关于类的静态数据成员

不可以在类的声明中初始化静态成员变量,因为声明描述了类如何分配内存,但并不分配内存。可以在类声明之外使用单独的语句来进行初始化,因为静态类成员是单独存储的,不是对象的组成部分。

静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时用作用域运算符来指出静态成员所属的类。但如果静态成员是整形或枚举型const,可以在类声明中初始化。

特殊的成员函数(类自动定义的)

默认构造函数(如果自己没有定义任何的构造函数)

编译器会自动生成一个不接受任何参数也不执行任何操作的构造函数(默认的默认构造函数),默认构造函数创建的对象未初始化,即它在被初始化之前成员的值都是未知的,不确定的。

我们可以自己定义默认构造函数(2种):

(1)不带有任何参数,但可以在函数内部为所有成员设置特定的值。

(2)带有参数,但所有参数都有默认值。

这2种不可以同时使用,否则会产生二义性。

默认析构函数

编译器会自动生成一个不执行任何操作的析构函数。

复制构造函数

当类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题。

默认复制构造函数执行浅复制,如果类对象需要在运行时动态分配内存,那么也应该提供一个显式复制构造函数来执行深复制。

赋值运算符

New 的注意事项

重载中括号

移动构造函数

移动赋值运算符

关于返回对象的说明

当类的成员函数返回对象时,有几种方式可以选择。

(1)返回指向const对象的引用

使用const 引用的常见原因旨在提高效率,如果函数返回(通过调用对象的方法或将对象作为参数)传递给他的对象,可以通过返回引用来提高效率。(因为返回对象会调用复制构造函数,而返回引用不会),当参数声明为const时,返回值也应该声明为const。

(2)返回指向非const对象的引用

有2种常见的情况:

重载赋值运算符以及重载与cout一起使用的<<运算符。前者旨在提高效率(避免调用复制构造函数,实现连续赋值只要返回对象就可以满足,而不在乎是否返回引用)。后者必须这么做(因为ostream没有公有的复制构造函数)。

(3)返回对象

如果被返回的对象是被调用函数中的局部变量,则不应该按照引用来返回。因为在函数执行完毕时,局部对象将调用其析构函数,引用所指向的对象将不存在。在这种情况应返回对象而不是引用(将调用复制构造函数来创建被返回的对象,这是不可避免的)。被重载的算术运算符都需要返回对象。

(4)返回const对象

将返回对象定义为const可以避免force1+force2=net编译通过而造成类对象的误用和滥用。

再谈初始化列表

非静态常量类成员只可以被初始化,不可以被赋值。

即:对于非静态const类成员和引用只可以通过初始化列表类初始化。(引用和常量一样只可以被初始化,而不可以被赋值)。

对于本身是类对象的成员来说,使用成员初始化列表效率更高。(这样只用调用复制构造函数,否则先调用构造函数,再调用赋值运算符)。

成员初始化列表只可以用于构造函数。

数据成员被初始化的顺序与它们出现在类声明中的顺序相同,而与初始化列表中的排列顺序无关。

十四 大端/小端

大端小端

大端字节序(存储模式):一个数据的低位字节序的内容存放在地址处,而高位字节序的内容存放在地址处。

小端字节序(存储模式):一个数据的高位字节序的内容存放在地址处,而低位字节序的内容存放在地址处。

判断大小端方法

假设一个数字是a,其值为1。a大端小端的内存分布图是:

实际上,我们只需要判断低地址处是否为1。当前系统为大端存储模式时,其低地址处存储00,而当前系统为小端存储模式时,其低地址处存储01。

方法一:

这个问题可简化为只判断一个字节的内容是否为1,而当前数字为整型,这里对该数字强制类型转换为char型即可。

 

方法二:用联合体去实现

网络通讯要求大端存储。

UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序;

十五 代码重用

代码重用的方法

(1)类中包含本身是另一个类对象的数据成员,即包含/组合。

(2)使用私有或保护继承。

通常,包含/私有/保护继承用于实现has-a关系,即新类包含另一个类的对象。

(3)使用类模板。

被包含对象的接口不是公有的,但可以在类方法中使用它。

私有继承

私有继承(Student继承自string和valarray)与包含(Student类数据成员包含string和valarray对象)的区别

(1)包含提供了2个被显式命名的对象成员,而私有继承提供了2个无名称的子对象成员。

(2)

(3)使用包含时,将使用对象名来调用方法。而使用私有继承将使用类名和作用域解析运算符来调用方法。

当私有继承访问基类对象时

私有继承访问基类的友元函数

如何确定使用包含还是继承?

多重继承

MI会带来很多问题

(1)从2个不同的基类继承同名方法

(2)从2个或更多相关基类那里继承同一个类的多个实例。

虚基类

菱形继承

为使得虚基类可以工作,需要对C++规则做些调整

1)构造函数

2)关于调用哪个方法

十六 C++/C与python区别

C/C++python的区别?

PYTHON是一种脚本语言,是解释执行的,不需要经过编译,所以很方便快捷,且能够很好地跨平台,写一些小工具小程序特别合适。

而C++则是一种需要编译后运行语言,在特定的机器上编译后在特定的机上运行,运行效率高,安全稳定。但编译后的程序一般是不跨平台的。

程序有两种执行方式,解释执行和编译执行。

解释程序

解释程序是高级语言翻译程序的一种,它将源语言(如BASIC)书写的源程序作为输入,解释一句后就提交计算机执行一句,并不形成目标程序。

这种工作方式非常适合于人通过终端设备与计算机会话,如在终端上打一条命令或语句,解释程序就立即将此语句解释成一条或几条指令并提交硬件立即执行且将执行结果反映到终端,从终端把命令打入后,就能立即得到计算结果。

但解释程序执行速度很慢,例如源程序中出现循环,则解释程序也重复地解释并提交执行这一组语句,这就造成很大浪费。

编译程序

把高级语言(如FORTRAN、COBOL、Pascal、C等)源程序作为输入,进行翻译转换,产生出机器语言的目标程序,然后再让计算机去执行这个目标程序,得到计算结果。

在实际应用中,对于需要经常使用的有大量计算的大型题目,采用招待速度较快的编译型的高级语言较好,虽然编译过程本身较为复杂,但一旦形成目标文件,以后可多次使用。相反,对于小型题目或计算简单不太费机时的题目,则多选用解释型的会话式高级语言,如BASIC,这样可以大大缩短编程及调试的时间。

十七 c++中创建由所有对象共享的常量

法一:在类中声明一个枚举,其作用域将为整个类。

Class  Bakery

{

Private:

Enum{Months=12};

Double  costs[Months];

}

使用这种方式声明的枚举不会创建类数据成员,所有对象中都不包含枚举,且Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器用12来替换他

法二:使用关键字static和const

Class  Bakery

{

Private:

Static const int Months=12;

Double  costs[Months];

}

创建一个名为Months的常量,与其他静态变量存储在一起。而不是存储在对象中,被所有对象共享。

十八 其它

C++中不要返回指向局部变量或临时对象的引用。函数执行完毕后,局部变量和临时对象将消失,引用会指向不存在的数据。

C/C++中从源文件到可执行程序的步骤: https://blog.csdn.net/qq_34342154/article/details/80583259

C++内存泄漏的原因  https://blog.csdn.net/qq_18824491/article/details/78902636

关于野指针 https://baike.baidu.com/item/野指针/9654046?fr=aladdin

空指针和void *指针  https://blog.csdn.net/luo_technically/article/details/52714389



这篇关于C++基础知识(2)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程