c++知识点
C++编译的过程
.c文件
1.预处理(Preprocessing),
- 展开所有的宏定义,消除“#define”;
- 处理所有的预编译指令,比如#if、#ifdef等;
- 处理#include预编译指令,将包含文件插入到该预编译的位置;
- 删除所有的注释“/**/”、”//“等;
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及错误提醒;
- 保留所有的#program编译指令,原因是编译器要使用它们;
–> .i文件
2.编译(Compilation),
- 编译过程就是把经过预编译生成的文件进行一系列语法分析、词法分析、语义分析优化后生成相应的汇编代码文件。
–> .s文件
3.汇编(Assemble)
将对应的汇编指令翻译成机器指令
生成可重定位的二进制文件
–>.o文件
4.链接(Linking)。
- 由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
- 链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)
链接分为两种:
- 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
- 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
二者的优缺点:
- 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
- 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失
https://blog.csdn.net/kang___xi/article/details/80210717
const
作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
const 的指针与引用
- 指针
- 指向常量的指针(pointer to const)
- 自身是常量的指针(常量指针,const pointer)
- 引用
- 指向常量的引用(reference to const)
- 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰
(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的
p2
、p3
使用
const 使用
1 | // 类 |
1 | int main() { |
const的实现机制
const在C语言中表示只读的变量,而在C++中表示一个常量。
1 | const int var = 10; |
1 | var=20 *ptr=20 |
C++语言中,const被看做常量,编译器使用常数直接替换掉被const修饰的标识符的引用,并不会通过访问内存去读取数据,这一点类似C语言中的宏#define。
1 | const int var = 10; |
1 | var=10 *ptr=20 |
此外,C++语言中,只是对于内置数据类型做常数替换,而对于像结构体这样的非内置数据类型则不会。因为结构体类型不是内置数据类型,编译器不知道如何直接替换,因此必须要访问内存去取数据,而访问内存去取数据必然会取到被指针q改变后的值,因此会造成与C++中const内置类型完全不一样的处理模式。
1 | struct test |
1 | var=40 *ptr=40 |
这是因为对于非内置数据类型,编译器不知道如何直接替换,所以对于var取值仍是通过读取它的存储空间中的值来获得。
总结一下,就是:const在C语言中表示只读的变量,而在C++中对于内置类型表示一个常量,对于非内置类型表示只读的变量。
宏定义 #define 和 typedef
宏定义 #define | typedef |
---|---|
宏定义,相当于字符替换 | 定义类型的别名 |
预处理器处理 | 编译器处理 |
无类型安全检查 | 有类型安全检查 |
不分配内存 | 要分配内存 |
存储在代码段 | 存储在数据段 |
可通过 #undef 取消 |
不可取消 |
没有作用域的限制 | 有自己的作用域 |
宏可能产生边界效应
#define MIN(a, b) a > b ? b : a
这句宏定义就会带来意想不到的问题,比如我在这样使用时:
num = c + MIN(num1, num2);
->num = c + a > b ? b : a
正确应该为:
#define MIN(a, b) (a > b ? b : a)
https://www.cnblogs.com/pam-sh/p/15232940.html
构造函数的执行顺序?析构函数的执行顺序?
虚基类 基类 派生类
1 |
|
1 | constructor - Person! |
将class Teacher : public Student,public Shit
变为class Teacher : public Shit,public Student
1 | constructor - Shit! |
- ==不会管谁继承得更深,如果没有虚基类的情况下按顺序构造和析构==。
将class Teacher : public Shit,public Student
变为class Teacher :public Student, public Virtual Shit
1 | constructor - Shit! |
==输出结果不变,说明优先构造虚基类==
在一个类中创建另一个类的实例
1 | class A{ |
1 | constructor A |
在一个类中创建另一个类的实例 并且有基类的情况
1 | class FB{ |
1 | constructor FB |
基类构造函数 > 成员构造函数 > 自身构造函数
构造函数的扩展过程?
记录在成员初始化列表中的数据成员的初始化操作会被放进该类的构造函数中,并以成员的声明顺序为顺序
如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;
在那之前,如果 class 有虚指针,那么它必须被设定初值,指向适当的虚表;
在那之前,所有上一层的基类构造函数必须被调用;
在那之前,所有虚基类的构造函数必须被调用。
成员初始化列表
成员初始化列表只能在构造函数中使用。
好处
- 更高效:少了一次调用默认构造函数的过程。
没有成员初始化列表的情况
假设一个类ClassA
有一个类型为Type
的成员变量member
。如果在构造函数体内对member
进行初始化,过程如下:
- 当创建
ClassA
的对象时,C++首先为所有成员变量调用默认构造函数(如果有的话)。这是在进入构造函数体之前自动发生的。- 默认构造函数指不需要任何参数即可调用的构造函数
- 显式定义的无参数构造函数:开发者在类中显式定义了一个不接受任何参数的构造函数
- 隐式定义的构造函数:如果你没有为类定义任何构造函数,C++编译器会自动生成一个默认构造函数,这个构造函数不执行任何操作。
- 默认构造函数指不需要任何参数即可调用的构造函数
- 然后执行构造函数体内的代码。如果此时对
member
进行了赋值或调用了其非默认构造函数进行初始化,实际上是在对已经默认构造的对象进行赋值操作,这可能导致了一次不必要的构造和随后的赋值操作。
使用成员初始化列表的情况
当使用成员初始化列表时,初始化的过程变得更直接:
- 使用成员初始化列表可以避免调用默认构造函数后再进行赋值的额外开销,直接调用成员的非默认构造函数
- 这意味着每个成员变量只被构造一次,并且是以最终需要的值进行构造,避免了不必要的默认构造和随后的赋值操作。
有些场合必须要用初始化列表:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化
派生类构造函数可以使用初始化器列表机制将位传递给基类构造函数。请看下面的例子:
1 | derived: :derived (type1 X, type2 y) base (x , y) // init ializer list |
其中derived 是派生类, base 是基类, x 和y 是基类构造函数使用的变量。例如,如果派生类构造函数接收到参数10 和12 ,则这种机制将把10 和12 传递给被定义为接受这些类型的参数的基类构造函数。除虚基类外(参见第14 章),类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,依此类推。如采没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。
- 成员初始化列表只能用于构造函数。
什么情况下会调用拷贝构造函数(三种情况)
类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。
- 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。
- 一个对象需要通过另外一个对象进行初始化。
C++移动构造函数(移动语义的具体实现)
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
static
作用
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 ==main 函数运行前就分配了空间==,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,==仅在定义该函数的文件内才能使用==。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
- 修饰成员变量,修饰成员变量使==所有的对象只保存一个该变量==,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 ==static 函数内不能访问非静态成员==。
- const和static不能同时修饰一个成员函数,因为C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
this 指针
this
指针是一个隐含于每一个==非静态成员函数==中的特殊指针。它==指向调用该成员函数的那个对象==。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 - 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为:ClassName *const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对this
指针所指向的这种对象进行修改(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。- 在以下场景中,经常需要显式引用
this
指针:- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。
inline 内联函数
特征
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- ==在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数==。
使用
1 | // 声明1(加 inline,建议使用) |
- 如下风格的函数 Foo 不能成为内联函数:
1 | inline void Foo(int x, int y); // inline 仅与函数声明放在一起 |
- 而如下风格的函数 Foo 则成为内联函数:
1 | void Foo(int x, int y); |
编译器对 inline 函数的处理步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
优缺点
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
面向对象
面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。
面向对象三大特征 —— 封装、继承、多态
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问
友元函数
- 在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。
1、为什么要引入友元函数:在实现类之间数据共享时,减少系统开销,提高效率
c++利用friend修饰符,可以让一些你设定的函数能够对这些保护数据进行操作,避免把类成员全部设置成public,最大限度的保护数据成员的安全。
具体来说:为了使其他类的成员函数直接访问该类的私有变量
即:允许外面的类或函数去访问类的私有变量和保护变量,从而使两个类共享同一函数(友元函数不是类的成员函数,是普通函数)
优点:能够提高效率,表达简单、清晰
缺点:友元函数破环了封装机制,尽量使用成员函数,除非不得已的情况下才使用友元函数。
2、什么时候使用友元函数:
1)运算符重载的某些场合需要使用友元。
1 | 假设重载了* |
对于结构体来说,如果要把重载运算符写在结构体内也需要使用friend,例如
1 | class node{ |
如果把friend删掉:
1 | //friend bool operator <(const node& a,const node& b){ |
会报如下错误:Overloaded 'operator<' must be a binary operator (has 3 parameters)
,意思是这个重载运算符的函数有三个参数,而重载运算符只能一目或二目。
不加friend时,这个函数相当于成员函数,当一个结构体的成员函数被调用时,和类一样,也会自动向这个成员函数传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针(this),所以会说传入了三个参数。但加了friend后,该函数就变成了一个普通函数,但是拥有访问结构体/类的私有参数的权利。但结构体中的参数默认是公有的,所以也可以直接将重载定义在结构体外面:
1 | class node{ |
但如果参数是私有的,就不行:
1 | class node{ |
2)两个类要共享数据的时候
1 |
|
1 | boy's name is:aaa,age:8 |
友元类
- 一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。
注意
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
C++中函数重载、隐藏、覆盖和重写的区别
1.函数重载(Function Overload)
1.1定义
C++规定在==同一作用域中==,同名函数的形式参数(指参数的个数、类型或者顺序)不同时,构成函数重载。
1.2用法
比如,要从两个变量中返回其中较大的一个值,可以编写如下两个构成重载的函数。
1 | int max(int a,int b){ |
复制
1.3注意事项
(1)函数返回值类型与构成函数重载无任何关系;
(2)类的静态成员函数与普通成员函数可以形成重载;
(3)函数重载发生在==同一作用域==,如类成员函数之间的重载、全局函数之间的重载。
2.函数隐藏(Function Hiding)
2.1定义
函数隐藏指**==不同作用域==**中定义的同名函数构成函数隐藏(==不要求函数返回值和函数参数类型相同==)。比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。请注意,==如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写==。
2.2用法用例
请仔细研读以下代码。
1 |
|
1 | 程序执行结果: |
2.3注意事项
对比函数隐藏与函数重载的定义可知:
(1)派生类成员函数与基类成员函数同名但参数不同。此时基类成员函数将被隐藏(注意别与重载混淆,重载发生在同一个类中);
(2)函数重载发生在同一作用域,函数隐藏发生在不同作用域。
3.函数覆盖与函数重写(Function Override)
网上和很多书籍多都会涉及函数覆盖的概念,众说纷纭,加大了许多初学者的学习难度,甚至产生误导。事实上,函数覆盖就是函数重写。
3.1定义
派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
关于返回值类型存在一种特殊情况,即协变返回类型(covariant return type)。
3.2虚函数重写与协变返回类型
如果虚函数函数返回指针或者引用时(不包括value语义),子类中重写的函数返回的指针或者引用是父类中被重写函数所返回指针或引用的子类型(这就是所谓的协变返回类型)[4]^{[4]}。看示例代码:
1 | #include <iostream> |
3.3注意事项
(1)函数覆盖就是虚函数重写,而不是函数被”覆盖”。 从上面的代码可以看出,函数是不可能被“覆盖”的。有些人可能会错误地认为函数覆盖会导致函数被”覆盖”而”消失”,将不能被访问,事实上只要通过作用域运算符::就可以访问到被覆盖的函数。因此,不存在被”覆盖“的函数。
(2)函数覆盖是函数隐藏的特殊情况。 对比函数覆盖和函数隐藏的定义,不难发现函数覆盖其实是函数隐藏的特例。
如果派生类中定义了一个==与基类虚函数同名但参数列表不同的非virtual函数==,则此函数是一个普通成员函数(非虚函数),并形成对基类中同名虚函数的隐藏,而非虚函数覆盖(重写)。
《C++高级进阶教程》中认为函数的隐藏与覆盖是两个不同的概念。隐藏是一个静态概念,它代表了标识符之间的一种屏蔽现象,而覆盖则是为了实现动态联编,是一个动态概念。但隐藏和覆盖也有联系:形成覆盖的两个函数之间一定形成隐藏。例如,可以对虚函数采用“实调用”,即尽管被调用的是虚函数,但是被调用函数的地址还是在编译阶段静态确定的,那么派生类中的虚函数仍然形成对基类中虚函数的同名隐藏。
参考如下代码,考察虚函数的实调用和虚调用。
1 |
|
复制
程序运行结果: In Base In Derived In Base In Derived In Base
4.总结
在讨论相关概念的区别时,抓住定义才能区别开来。C++中函数重载隐藏和覆盖的区别,并不难,难就难在没弄清定义,被网上各种说法弄的云里雾里而又没有自己的理解。
在这里,牢记以下几点,就可区分函数重载、函数隐藏、函数覆盖和函数重写的区别:
(1)函数重载发生在相同作用域;
(2)函数隐藏发生在不同作用域;
(3)函数覆盖就是函数重写。准确地叫作虚函数覆盖和虚函数重写,也是函数隐藏的特例。
关于三者的对比,李健老师在《编写高质量代码:改善C++程序的150个建议》给出了较为详细的总结,如下表所示:
三者 | 作用域 | 有无virtual | 函数名 | 形参列表 | 返回值类型 |
---|---|---|---|---|---|
重载 | 相同 | 可有可无 | 相同 | 不同 | 可同可不同 |
隐藏 | 不同 | 可有可无 | 相同 | 可同可不同 | 可同可不同 |
重写 | 不同 | 有 | 相同 | 相同 | 相同(协变) |
动态联编与静态联编
在 C++ 中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;
静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。
动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。
C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)
实现动态联编三个条件:
必须把动态联编的行为定义为类的虚函数;
类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来;
必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数;
面向对象
继承
- 基类(父类)——> 派生类(子类)
多态
- 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
- 多态是以封装和继承为基础的。
- C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
静态多态(编译期/早绑定)
函数重载
1 | class A |
void * 指针(泛型指针)
https://zhuanlan.zhihu.com/p/163676489?utm_source=wechat_session&utm_id=0
void即为不确定类型——类型不确定从而所占内存不确定,所以诸如void par = 10;
之类的声明是万万不可的,即void类型不能声明实例对象。在C语言中,void的作用主要有以下两大类:
- 对函数返回类型的限定,利用void对象的大小不确定来限制函数不能有任何返回值——这就是我们常写的void作返回值的函数。
- 对函数参数类型的限定,当函数不允许接受参数是,必须用void来限定函数的参数——当然现在没什么会这么写了:
int func(void);
。
但void*
则不同,编译器会允许你做类似于int someInt = 10; void* par = &someInt;
之类的操作,因为无论指向什么类型的指针,指针本身所占空间是一定的。我们可以认为void*
就是一个通用指针,可以指向任意类型的指针。我们都知道,指针有两个属性:指向变量/对象的地址和长度,但是指针指存储被指向变量的地址,长度则取决于指针的类型,编译器根据指针的类型从指针指向的地址向后寻址,不同的类型则寻址范围不同,如int*
从指定地址向后寻找4字节作为变量的存储单元。而我们将一个void
类型的指针指向一个int类型的实例,实际上是抹去了这一实例的类型信息,因此在使用时我们要在心里清楚被抹去的类型信息。
动态多态(运行期/晚绑定)
- 虚函数:用 virtual 修饰成员函数,使其成为虚函数
- 动态绑定:当使用基类的引用或指针调用一个虚函数时将发生动态绑定
注意:
- 可以将派生类的对象赋值给基类的指针或引用,反之不可
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 内联函数不能是表现多态性时的虚函数。
虚函数(virtual)可以是内联函数(inline)吗?
Are “inline virtual” member functions ever actually “inlined”?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
虚函数内联使用
1 |
|
动态多态使用
1 | class Shape // 形状类 |
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
虚析构函数使用
1 | class Shape |
析构函数一般写成虚函数的原因
直观的讲:是为了降低内存泄漏的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那 么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。
纯虚函数
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
1 | virtual int A() = 0; |
虚函数、纯虚函数
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类,具体见虚继承。
虚函数指针、虚函数表
- 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
- 虚函数表:是编译器在编译时期为我们创建好的, 只存在一份。在程序只读数据段(
.rodata section
,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
虚继承
https://blog.csdn.net/galaxyrt/article/details/118118831
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,8字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
虚继承、虚函数
- 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
- 不同之处:
- 虚继承
- 虚基类依旧存在继承类中,只占用存储空间
- 虚基类表存储的是虚基类相对直接继承类的偏移
- 虚函数
- 虚函数不占用存储空间
- 虚函数表存储的是虚函数地址
- 虚继承
模板类、成员模板、虚函数
- 模板类中可以使用虚函数
- 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数
抽象类、接口类、聚合类
- 抽象类:含有纯虚函数的类
- 接口类:仅含有纯虚函数的抽象类
- 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类,也没有 virtual 函数
计算下面几个类的大小
1 | class A{}; sizeof(A) = 1; //空类在实例化时得到一个独一无二的地址,所以为 1. |
各种类型的大小
关于虚函数
- 为什么调用普通函数比调用虚函数的效率高?
1 | 因为普通函数是静态联编的,而调用虚函数是动态联编的。 |
- 为什么要用虚函数表(存函数指针的数组)?
1 | 实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。 |
- 为什么要把基类的析构函数定义为虚函数?
1 | 在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.如下代码: |
- 子类是否要重写父类的虚函数?
子类继承父类时, 父类的纯虚函数必须重写,否则子类也是一个虚类不可实例化。 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
虚函数上的缺省参数是不会有多态行为的,所以以下代码输出Derive:3+2
1 |
|
为什么构造函数不能是虚函数
- 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间(运行时)的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
- 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
如何限制类的对象只能在堆上创建?如何限制对象只能在栈上创建?
说明:C++ 中的类的对象的建立分为两种:静态建立、动态建立。
- 静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:
A a;
- 动态建立:使用
new
关键字在堆空间上创建对象,底层首先调用operator new()
函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A();
限制对象只能建立在堆上:
最直观的思想:避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行,因为当构造函数设置为私有后,不能在类的外部调用构造函数来构造对象,只能用
new
来建立对象。但是由于new
创建对象时,底层也会调用类的构造函数,将构造函数设置为私有后,那就无法在类的外部使用new
创建对象了。因此,这种方法不可行。解决方法 1:
将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。
- C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14class A
{
public:
A() {}
void destory()
{
delete this;
}
private:
~A()
{
}
};该方法存在的问题:
用
new
创建的对象,通常会使用delete
释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个destory()
函数,用来释放new
创建的对象。无法解决继承问题,因为如果这个类作为基类,析构函数要设置成
virtual
,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。
解决方法 2:
构造函数设置为
protected
,并提供一个public
的静态函数来完成构造,而不是在类的外部使用new
构造;将析构函数设置为protected
。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。- C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class A
{
protected:
A() {}
~A() {}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};
限制对象只能建立在栈上:
解决方法:将
operator new()
设置为私有。原因:当对象建立在堆上时,是采用new
的方式进行建立,其底层会调用operator new()
函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。C++
1 | class A |
volatile
1 | volatile int i = 10; |
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
- volatile 关键字声明的变量,每次访问时都==必须从内存中取出值==(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
- const 可以是 volatile (如只读的状态寄存器)
- 指针可以是 volatile
const和volatile 也一样,所谓的const,只是编译器保证在C的“源代码”里面,没有对该变量进行修改的地方,而实际运行的时候则不是编译器所能管的了。
同样,volatile的所谓“可能被修改”,是指“在运行期间”可能被修改。也就是告诉编译器,这个变量不是“只”会被这些C的“源代码”所操纵,其它地方也有操纵它们的地方。所以,C编译器就不能随便对它进行优化了。
const –>该变量为常量,不能在此程序中更改
volotile –>该变量为一个共享变量,也就是说会有除了本程序之外的其他途径对其值进行更改,如多线程,或是硬件,其他的运行程序.
const volatile表示该变量既不能被修改,又不能被优化到寄存器,即又是可能会被其他编译器不知道的方式修改的。比如一个实时时钟
,我们不希望被程序做修改,所以要声明为const,但其他的线程、中断等(可能来自于库)又要修改此时钟的值,编译器不能把它作为const常量优化到寄存器,所以又要声明为volatile。再举个例子,只读的状态寄存器
,它是volatile,因为它可能被意想不到地改变。它是const,因为程序不应该试图去修改它。
assert()
断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>
(C)、<cassert>
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include <assert.h>
之前。
assert() 使用
1 |
|
extern “C”
- 被 extern 限定的函数或变量是 extern 类型的
- 被
extern "C"
修饰的变量和函数是按照 C 语言方式编译和链接的
extern "C"
的作用是让 C++ 编译器将 extern "C"
声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
extern “C” 使用
1 |
|
C++中的链接属性
链接属性一定程度范围决定着符号的作用域,C++中链接属性有三种:none(无)、external(外部)和 internal(内部)。
- external,外部链接属性。非常量全局变量和自由函数(除成员函数以外的函数)均默认为外部链接的,它们具有全局可见性,在全局范围不允许重名,详情可见例子。
- internal,内部链接属性。具有该属性的类型有,const对象,constexpr对象,命令空间内的静态对象(static objects in namespace scope)
- none,在类中、函数体和代码块中声明的变量默认是具有none链接属性。它和internal一样只在当前作用域可见。
extern的用法
extern有3种用法,分别如下:
非常量全局变量的外部链接
最常见的用法,当链接器在一个全局变量声明前看到extern关键字,它会尝试在其他文件中寻找这个变量的定义。这里强调全局且非常量的原因是,全局非常量的变量默认是外部链接的。
1 | //fileA.cpp |
常量全局变量的外部链接
常量全局变量默认是内部链接的,所以想要在文件间传递常量全局变量需要在定义时指明extern,如下所示:
1 | //fileA.cpp |
extern “C” 和extern “C++”函数声明
在C++中,当与字符串连用时,extern指明当前声明使用了其他语言的链接规范,如extern “C”,就指明使用C语言的链接规范。原因是,C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时无法找到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。C和C++对函数的处理方式是不同的.extern “C”是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern “C”来说明。
例子如下:
1 | // 声明printf函数使用C链接 |
- 使用extern和包含头文件来引用函数有什么区别呢?
与include相比,extern引用另一个文件的范围小,include可以引用另一个文件的全部内容。extern的引用方式比包含头文件要更简洁。extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。
注意事项
- 不要把变量定义放入.h文件,这样容易导致重复定义错误
- 尽量使用static关键字把变量定义限制于该源文件作用域,除非变量被设计成全局的。
- 可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。
struct 和 typedef struct
C 中
1 | // c |
等价于
1 | // c |
此时 S
等价于 struct Student
,但两个标识符名称空间不相同。
另外还可以定义与 struct Student
不冲突的 void Student() {}
。
C++ 中
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {...};
,使用 Student me;
时,编译器将搜索全局标识符表,Student
未找到,则在类标识符内搜索。
即表现为可以使用 Student
也可以使用 struct Student
,如下:
1 | // cpp |
二、若定义了与 Student
同名函数之后,则 Student
只代表函数,不代表结构体,如下:
用typedef可以声明各种类型名,但不能用来定义变量。
1 | typedef struct Student { |
C++ 中 struct 和 class
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别
- 最本质的一个区别就是默认的访问控制
- 默认的继承访问权限。struct 是 public 的,class 是 private 的。
- struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
union 联合
联合(union)是一种==节省空间的特殊的类==,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
共用体变量所占的内存长度是所有成员中最长的成员的长度
默认访问控制符为 public
可以含有构造函数、析构函数
不能含有引用类型的成员
不能继承自其他类,不能作为基类
==不能含有虚函数==
匿名 union 在定义所在作用域可直接访问 union 成员
匿名 union 不能包含 protected 成员或 private 成员
全局匿名联合必须是静态(static)的
union 使用
1 |
|
explicit(显式)关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
explicit 使用
1 | struct A |
C++ 的四种强制转换
C++ 的四种强制转换包括:static_cast, dynamic_cast, const_cast, reinterpret_cast
- static_cast:明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上⾏转换
(派生类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以主要执⾏⾮多态的转换操作;
dynamic_cast:专⻔⽤于派⽣类之间的转换,type-id 必须是类指针,类引⽤或 void*,对于下⾏转换是安全的,当类型不⼀致时,转换过来的是空指针,⽽static_cast当类型不⼀致时,转换过来的是错误意义的指针,可能造成⾮法访问等问题。
const_cast:专⻔⽤于 const 属性的转换,去除 const 性质,或增加 const 性质, 是四个转换符中唯⼀⼀个可以操作常量的转换符。
reinterpret_cast:不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。
C++ 中的指针参数传递和引用参数传递
指针参数传递本质上是值传递,它所传递的是⼀个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的⼀个副本(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变量,但是任何对于引用参数的处理都会通过⼀个间接寻址的⽅式操作到主调函数中的相关变量。⽽对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使⽤指向指针的指针或者指针引⽤。
简单说一下函数指针
⾸先是定义:函数指针是指向函数的指针变量。函数指针本身⾸先是⼀个指针变量,该指针变量指向⼀个具体的函数。这正如⽤指针变量可指向整型变量、字符型、数组⼀样,这⾥是指向函数。
在编译时,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。有了指向函数的指针变量后,可⽤该指针变量调⽤函数,就如同⽤指针变量可引⽤其他类型变量⼀样,在这些概念上是⼤体⼀致的。
其次是⽤途:调⽤函数和做函数的参数,⽐如回调函数。
示例:
1 | char * fun(char * p) {…} // 函数fun |
:: 范围解析运算符(不能被重载)
分类
- 全局作用域符(
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
:: 使用
1 | int count = 11; // 全局(::)的 count |
引用
左值和右值
左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。通常来说有名字的变量就是左值(如上面例子中的 a, b),而由运算操作(加减乘除,函数调用返回值等)所产生的中间结果(没有名字)就是右值。
可以简单认为: 左值就是在程序中能够寻值的东西,右值就是没法取到它的地址的东西(不完全准确)
左值引用&
常规引用,一般表示对象的身份。
右值引用&&
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用的好处是能够利用右值引用实现移动语义的库代码
拷贝构造函数和移动构造函数:例如A a(10+20)
时,因为10+20
是一个右值,拷贝构造函数会创建临时对象temp,然后将temp值赋给a,(此时temp和a是两个不同的地址),对象a使用刚刚temp被释放的内存
而移动构造函数也会创建临时变量temp,但所创建就是临时变量的地址就是之后用于a的地址,不会有额外的复制工作。
注意事项
移动构造函数和移动赋值运算符的参数不能是const引用,因为修改了源对象
想让左值也调用移动构造/赋值运算符,用
move(x)
,例如a=move(b)
,b是一个左值,但此时调用的移动赋值运算符int &&r=5+6
5+6是不能被取地址的,但r
是可以被取地址的
https://zhuanlan.zhihu.com/p/335994370
引用折叠
X& &
、X& &&
、X&& &
可折叠成X&
X&& &&
可折叠成X&&
内存分配和管理
malloc、calloc、realloc、alloca
- malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
- calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
- realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
- alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
malloc、free
用于分配、释放内存
malloc、free 使用
申请内存,确认是否申请成功
1 | char *str = (char*) malloc(100); |
释放内存后指针置空
1 | free(p); |
new、delete
- new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
- delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
- new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。
new、delete 使用
申请内存,确认是否申请成功
1 | int main() |
定位 new
定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。
1 | new (place_address) type |
place_address
是个指针initializers
提供一个(可能为空的)以逗号分隔的初始值列表
new与malloc区别
https://www.cnblogs.com/QG-whz/p/5140930.html
创建位置
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了。new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
delete this 合法吗?
Is it legal (and moral) for a member function to say delete this?
合法,但:
- 必须保证 this 对象是通过
new
(不是new[]
、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的 - 必须保证调用
delete this
的成员函数是最后一个调用 this 的成员函数 - 必须保证成员函数的
delete this
后面没有调用 this 了 - 必须保证
delete this
后没有人使用了
智能指针
C++ 标准库(STL)中
头文件:#include <memory>
C++ 11
- shared_ptr
- unique_ptr
- weak_ptr
- auto_ptr(被 C++11 弃用)
1、auto_ptr(C++98 的⽅案,C11 已抛弃)采⽤所有权模式。
1 | auto_ptr<std::string> p1 (new string ("hello")); |
此时不会报错,p2 剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr 的缺点是:存在潜在的内存崩溃问题!
2、unique_ptr(替换 auto_ptr )
unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露特别有⽤。
采⽤所有权模式,还是上⾯那个例⼦
1 | unique_ptr<string> p3 (new string (auto));//#4 |
编译器认为 p4=p3 ⾮法,避免了 p3 不再指向有效数据的问题。
因此,unique_ptr ⽐ auto_ptr 更安全。
3、shared_ptr(共享型,强引⽤)
shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后⼀个引⽤被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。
可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr,unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
4、weak_ptr(弱引⽤)
weak_ptr 是⼀种不控制对象⽣命周期的智能指针,它指向⼀个 shared_ptr 管理的对象。进⾏该对象的内存管理的是那个强引⽤的 shared_ptr。weak_ptr 只是提供了对管理对象的⼀个访问⼿段。weak_ptr 设计的⽬的是为配合 shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造,,它的构造和析构不会引起引⽤记数的增加或减少。weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以。
1 |
|
1 |
|
share_ptr的线程安全性
shared_ptr的析构函数需要在使其他分享同一对象的所有权的shared_ptr实例的use_count()汇报数字减1,而C++标准规定对use_count()结果的变更不可以成为data race,于是析构函数不可以以一种产生data race的方式去修改use_count()(及其底层对应的计数)。use_count在这里面汇报的是“强引用个数”,即不包含weak_ptr只计数shared_ptr的引用计数,故而析构函数对这个引用计数的修改必须是线程安全的,否则产生data race即违反了C++标准。而上一话题中提到的跨线程分享ownership的一组shared_ptr的并发析构,也就是安全的了,因为标准保证了析构函数对“强引用计数”无data race,故而它会被安全地逐个减1,并最终在导致减到0的那个线程上析构被分享的对象。
结论:对同一个对象分享所有权的shared_ptr在多个线程上的析构不需要外部加锁保护,C++标准以及标准的实现保证这一动作的线程安全性。
手写share_ptr
1 | template<typename T> |
size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int.
内存泄漏的几种情况
内存泄漏简单的说就是申请了一块内存空间,使用完毕后没有释放掉。 它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄漏了。
- 在类的构造函数和析构函数中没有匹配的调用new和delete函数
两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存
没有正确地清除嵌套的对象指针
在释放对象数组时在delete中没有使用方括号
方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄漏。
释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。
- 指向对象的指针数组不等同于对象数组
对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间
指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了
- 缺少拷贝构造函数(浅拷贝和深拷贝的问题)
两次释放相同的内存是一种错误的做法,同时可能会造成堆的崩溃。
按值传递会调用(拷贝)构造函数,引用传递不会调用。
在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。
所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
- 缺少重载赋值运算符
这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露,如下图:
- 关于nonmodifying运算符重载的常见迷思
a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针
b. 返回内部静态对象的引用。
c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收
解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int
- 没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
野指针:没有被初始化的指针
造成野指针的原因:
指针变量没有被初始化(如果值不定,可以初始化为NULL)
指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。
悬空指针:是指针最初指向的内存已经被释放了的一种指针。
指针被free或者delete后,没有置为NULL, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL.
内存泄漏检测工具的实现原理:
内存检测工具有很多,这里重点介绍下 valgrind 。
valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合,包括以下工具:
- Memcheck:内存检查器(valgrind 应用最广泛的工具),能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
- Callgrind:检查程序中函数调用过程中出现的问题。
- Cachegrind:检查程序中缓存使用出现的问题。
- Helgrind:检查多线程程序中出现的竞争问题。
- Massif:检查程序中堆栈使用中出现的问题。
- Extension:可以利用 core 提供的功能,自己编写特定的内存调试工具。
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表:
- Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits ;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
- Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。
检测原理:
- 当要读写内存中某个字节时,首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置,Memcheck 则报告读写错误。
- 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 Memcheck 会检查 Valid-Value 表对应的 bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
c++中函数被调用的过程
https://www.zhihu.com/question/22444939/answer/22200552
堆栈平衡(栈帧调整):具体包括
保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)
将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)
给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶)
C++ 中内存分配情况
栈
由编译器自动分配和释放,一般保存的是局部变量和函数参数等。
连续的内存空间,在函数调用的时候,首先入栈的主函数的下一条可执行指令的地址,然后是函数的各个参数。
大多数编译器中,参数是从右向左入栈(原因在于采用这种顺序,是为了让程序员在使用C/C++的“函数参数长度可变”这个特性时更方便。如果是从左向右压栈,第一个参数(即描述可变参数表的各变量类型的那个参数)将被放在栈底,由于可变参的函数第一步就需要解析可变参数表的各参数类型,即第一步就需要得到上述参数,因此,将它放在栈底是很不方便的。)本次函数调用结束时,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运行,不会产生碎片。
栈是高地址向低地址扩展,栈低高地址,空间较小。
堆
由程序员管理,需要手动 new malloc delete free 进行分配和回收,如果不进行回收的话,会造成内存泄漏的问题。
不连续的空间,实际上系统中有一个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第一个大于等于申请大小的空间分配给程序,一般在分配程序的时候,也会空间头部写入内存大小,方便 delete 回收空间大小。当然如果有剩余的,也会将剩余的插入到空闲链表中,这也是产生内存碎片的原因。
堆是低地址向高地址扩展,空间较大,较为灵活。
全局/静态存储区
分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
常量存储区:存储常量,⼀般不允许修改。
代码区
存放程序的⼆进制代码。
栈和堆的区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请。
- 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
- 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
- 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
- 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
c++11新特性
C++ 模板是什么,底层怎么实现的?
https://zhuanlan.zhihu.com/p/101898043
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产⽣不同的函数;编译器会对函数模板进⾏两次编译:在声明的地⽅对模板代码本身进⾏编译,在调⽤的地⽅对参数替换后的代码进⾏编译。
这是因为函数模板要被实例化后才能成为真正的函数,在使⽤函数模板的源⽂件中包含函数模板的头⽂件,如果该头⽂件中只有声明,没有定义,那编译器⽆法实例化该模板,最终导致链接错误。
请你来写个函数在 main 函数执行前先运行
1 | //第⼀种:gcc扩展,标记这个函数应当在main函数之前执⾏。同样有⼀个__attribute((destructor)),标记函数应当在程序结束之前(main结束之后,或者调⽤了exit后)执⾏; |
请你来说⼀下 fork 函数
1 | #Fork:创建⼀个和当前进程映像⼀样的进程可以通过 fork() 系统调⽤: |
成功调⽤ fork() 会创建⼀个新的进程,它⼏乎与调⽤ fork() 的进程⼀模⼀样,这两个进程都会继续运行。在⼦进程中,成功的 fork( ) 调⽤会返回0。在⽗进程中 fork() 返回⼦进程的 pid。
如果出现错误,fork() 返回⼀个负值。
最常⻅的 fork() ⽤法是创建⼀个新的进程,然后使⽤ exec() 载⼊⼆进制映像,替换当前进程的映像。这种情况下,派⽣(fork)了新的进程,⽽这个⼦进程会执⾏⼀个新的⼆进制可执⾏⽂件的映像。这种“派⽣加执⾏”的⽅式是很常⻅的。
在早期的 Unix 系统中,创建进程⽐较原始。当调⽤ fork 时,内核会把所有的内部数据结构复制⼀份,复制进程的⻚表项,然后把⽗进程的地址空间中的内容逐⻚的复制到⼦进程的地址空间中。但从内核⻆度来说,逐⻚的复制⽅式是⼗分耗时的。现代的 Unix 系统采取了更多的优化,例如 Linux,采⽤了写时复制的⽅法,⽽不是对⽗进程空间进程整体复制。
简单说⼀下 printf 实现原理?
在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压⼊堆栈的⽅式来给函数传参数的(堆栈是⼀种先进后出的数据结构)。
最先压⼊的参数最后出来,在计算机的内存中,数据有 2 块,⼀块是堆,⼀块是栈(函数参数及局部变变量在这⾥),⽽栈是从内存的⾼地址向低地址⽣⻓的,控制⽣⻓的就是堆栈指针了,最先压⼊的参数是在最离⾯,就是说在所有参数的最后⾯,最后压⼊的参数在最外⾯,结构上看起来是第⼀个,所以最后压⼊的参数总是能够被函数找到。因为它就在堆栈指针的上方。printf的第⼀个被找到的参数就是那个字符指针,就是被双引号括起来的那⼀部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移了。
- 注意
1 |
|
输出
3,3
1 | x=1; printf("%d %d\n",x,x++); |
输出
1 | 2 1 |
在计算时,遇到x++会记录此时的x的值作为最后的输出结果。遇到x和++x的时候则不会将此时的计算结果作为最终的输出,只会修改x的值,在最终输出的时候都输出x的值(所以++x和x的结果总是一样的)。
为什么会是这个样子呢?参见某高手解释吧:
对于a++的结果,是有ebp寻址函数栈空间来记录中间结果的,在最后给printf压栈的时候,再从栈中把中间结果取出来;而对于++a的结果,则直接压寄存器变量,寄存器经过了所有的自增操作。
1 | a=1; |
输出6 7 4 7 2 7
a++的值在运算过程中就能确定;
而所有++a或者a(如果有)的值是所有对a的值产生影响的运算之后最终的值。
手写字符串函数 strcat,strcpy,strncpy,memset,memcpy实现
1 | //把 src 所指向的字符串复制到 dest,注意:dest定义的空间应该⽐src⼤。 |
1.memcpy和memmove相同点
都是用于从src拷贝count个字节到dest。
2.memcpy和memmove区别
如果目标区域和源区域有重叠的话:
memcpy不能够确保源串所在重叠区域在拷贝之前被覆盖。
memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后src内容会被更改,当目标区域与源区域没有重叠则和memcpy函数功能相同。
但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。
memmove的处理措施:
(1)当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
(2)当源内存的首地址大于目标内存的首地址时,实行正向拷贝
(3)当源内存的首地址小于目标内存的首地址时,实行反向拷贝
memcmp函数
通过输入字节数n,比较前后两个数组从首地址开始的n个字节
例子:
1 | #include<stdio.h> |
arr1在内存中的样子
arr2在内存中的样子
当比较16个字节时,相等
当比较17个字节时,依然是相等
原因是:在第17个字节处刚好相等(这里是小端字节序)
当比较18个字节时结果是-1
原因是在第18个字节处出现了不一样,arr2是33,arr1是00
C或C++中的位域(bit field)
位段(或称“位域”,Bit field)为一种数据结构,可以把数据以位元的形式紧凑的储存,并允许程序员对此结构的位元进行操作。这种数据结构的好处:
可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。位段可以很方便的访问一个值的部分内容从而可以简化程序源代码。
用法
只可以在struct或者class中使用。具体用法如下:
比如,我们需要定义一个结构体来储存一些信息,我们可以这样写
1 | struct CHAR2 |
其实上面的写法已经使得储存空间很紧凑了,但是我们可以用bit field进一步减少空间占用。
1 | struct CHAR |
可以看出,第一种写法需要使用48bit的空间来储存信息,但第二种写法(bit field)只需要30bit的空间就可以储存相同的信息。当然这种写法的前提是,我们知道将要储存数据的范围。
位域的大小
例如以下位域:
1 | struct box |
该位域结构体中间有一个未命名的位域,占据 3 Bits,仅起填充作用,并无实际意义。 填充使得该结构总共使用了 8 Bits。但 C 语言使用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同。 有些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。文章以下均默认 unsigned int 为 32 Bits。
decltype
https://blog.csdn.net/u014609638/article/details/106987131/
1 | int a=3,b=4; |
Type | Value | |
---|---|---|
a | int | 4 |
b | int | 4 |
c | int | 4 |
d | int & | 4 |
a是一个变量,所以decltype返回的类型就是int
而a=b是一个左值表达式,decltype接受的是左值表达式,返回左值引用;接受的是右值表达式,返回右值的类型,所以d的类型是int &,又因为decltype里的表达式不会真的被计算,所以a还是3。又因为d是引用a,所以++d同时也会改变a
运算符优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
() | 圆括号 | (表达式)/函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 前置自增运算符 | ++变量名 | 单目运算符 | ||
++ | 后置自增运算符 | 变量名++ | 单目运算符 | ||
– | 前置自减运算符 | –变量名 | 单目运算符 | ||
– | 后置自减运算符 | 变量名– | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式/整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | 从左向右顺序运算 |
左结合和右结合
a=b+c+d
=是右结合的,所以先计算(b+c+d),然后再赋值给a
+是左结合的,所以先计算(b+c),然后再计算(b+c)+d
C++中#(一个#号)和##(两个#号)的用法和作用
C/C++ 的宏中,#的功能是将其后面的宏参数进行字符串化操作,简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。
1 |
|
##连接符号由两个井号组成,其功能是在带参数的宏定义中将两个子串联接起来,从而形成一个新的子串。但它不可以是第一个或者最后一个子串。
1 | #define MERGE1(x,y) x##y |
各种阅读程序题
答案为-6
- 前缀++和–的优先级比*作为乘号时高(和*作为取指针指向值时相同)
答案为32
- ++l先+了再返回结果,模拟一遍即可
1 |
|
输出就是66 65,不涉及继承和其他奇怪的操作
s1.s.d[0]
被设置为1
时,这个字节现在和s2
的相应字节(仍然是0
,因为s2.s.d
被清零了)不同。根据memcmp
的比较规则,因为s1
的相应字节1
在ASCII表中小于s2
的相应字节0
,所以memcmp
返回-1。
不难理解答案是-1
答案是3
1 |
|
&a
获取的是数组a
的地址,但请注意它是一个指向整个数组的指针,类型为int (*)[10]
。当对&a
执行+1
操作时,指针会跳过整个数组a
的内存大小,即它将指向数组a
之后的内存地址。换句话说,&a + 1
不是指向数组第一个元素的下一个元素的指针,而是跳过了整个数组的指针。
因此,ptr
实际上是指向a
数组尾部后的那个整数的地址。现在,考虑到数组a
有10个元素,所以ptr
指向的是a[10]
(注意这是一个超出数组边界的位置)。
当你做*(ptr - 6)
操作时,你实际上是访问a[10 - 6]
,即a[4]
。根据数组的初始化,a[4]
的值是5。
答案是5
首先注意,虽然a和&a[0]的地址相同但是&a+1和&a[0]+1(a+1)的含义是不同的
一个int是4个字节,所以&a+1相当于+了4*10。再-6*4,就是a[4]的位置。
1 | int main(){ |
注意这是大根堆
输出853
1 |
|
输出为5
因为结构体不像数组,例如int a[10]
, a就是指向数组首地址的指针,而这个地方struct node *pt=&s;
,pt只是结构体s的首地址,而并不是指针,所以要加一个(int *)把这个地址转换为指针,才能对指针+1之后再解引用,如果不加int *
会报错