c++基础问题

41 篇文章 0 订阅

目录

 

指针与引用

a)区别

b)什么时候用指针?什么时候用引用?

c)野指针,空悬指针

智能指针

C++11的特性(构造函数的初始化列表,智能指针,lambda)

 析构函数能否抛出异常

c与c++

面向对象的三个基本特征是:封装、继承、多态

多态、 RTTI、RAII

析构函数为什么要虚函数?

构造函数为什么不能是虚函数?

析构和构造函数体内能调用虚函数吗?

C++纯虚函数

虚表指针,虚拟继承,多重继承

struct和class区别

空类(没有数据成员)

内存对齐

红黑树与avl树

C++四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast

函数指针

static

++i,和i++ 重载

手写memcpy,strcpy,memset,strcat,strcmp

手写单例

重载、隐藏、覆盖

stl容器是否线程安全?

在main函数执行前运行

C++内存管理

new/malloc 的区别

以下四行代码的区别是什么?


 

指针与引用

a)区别

指针:是一个变量,它存的是所指对象的地址。

而引用是所指对象的别名,其实引用实际上就是一个自带解引用的指针常量

因为是常量,所以

  1. 必须初始化(不能是nullptr),不存在指向空值的引用,但是存在指向空值的指针。
  2. 必须从一而终,指了某个对象就不能再变了
  3.  所以没有常量引用这种东西

又因为自带解引用,所以我们可以把引用看成是所指对象的另一个名字。

b)什么时候用指针?什么时候用引用?

可能不指向任何东西或者可能在不同的时候会指向不同的对象的时候用指针,而总是指向某一个对象的时候,用引用.

参考: https://www.zhihu.com/question/37608201 

            more effective c++ p11 

c)野指针,空悬指针

野指针: 没有初始化的指针,一般情况下,编译器会进行警告。

空悬指针:指向已经销毁的对象或已经回收的地址。

空悬指针指向的那块内存,可能又被分配给别人了,所以就可能出现灾难性的后果。而野指针的它值会是上一次使用保存下来的值,所以就会一顿瞎指,所以也是灾难性的后果。

解决办法:

1.  delete之后马上赋为nullptr。 

2。 用到这个变量才去定义这个变量。

智能指针

https://blog.csdn.net/speargod/article/details/100058789

C++11的特性(构造函数的初始化列表,智能指针,lambda)

https://blog.csdn.net/speargod/article/details/101071914

 析构函数能否抛出异常

c++并不禁止在析构函数里抛出异常,但是绝对不应该写出会抛出异常的析构函数。

1)首先因为当异常发生的时候,c++的机制会调用已经构造对象的析构函数来释放资源,比如我们为某个类A创建size为10的vector,挨个为它执行默认构造函数,假如执行到第5个元素的时候,抛出异常了,c++这时候又会从第4个到第1个倒着执行析构函数,假如这时候析构函数又抛出了异常,ok,那现在程序里面有两个未被解决的异常,那程序直接被结束掉。

2)并且假如析构函抛出异常,那么函数体内之后的代码就不会被执行,而如果析构函数在异常点之后需要执行某些必要的动作比如释放某些资源,则这些动作不会执行,这就会造成诸如资源泄漏的问题。

c与c++

C是面向过程的,而C++是面向对象的,按effective c++里面的说法就是,c++是一个语言联邦,包含C,C with classes,模板,还有STL。

面向对象的三个基本特征是:封装、继承、多态

https://blog.csdn.net/speargod/article/details/101022333

多态、 RTTI、RAII

https://blog.csdn.net/speargod/article/details/100127395

析构函数为什么要虚函数?

为了不发生资源泄露。具体的说确保当我们删除一个基类指针,而该指针实际上指向的是一个派生类对象时,也能够调用派生类的析构函数,正确的销毁整个对象。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

构造函数为什么不能是虚函数?

因为一个指针去调用一个虚函数的实现机制是,编译器把

                                 ptr->z(); //被编译器转化为: (*ptr->vptr[4])(ptr);

也就是说一个指向类的指针调用虚函数是通过这个放在类对面里面固定偏移位置的虚函数表指针(vptr),而这个vptr指向一个元素是指向虚函数的指针 的数组,通过数组里面的固定下标来拿到所要执行的真正的函数。

简单说就是:假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,我们就没有虚函数表指针,也就没办法找vtable呢?所以构造函数不能是虚函数。

析构和构造函数体内能调用虚函数吗?

能,但是不要,取得的效果可能跟我们预想的不一样。因为先构造基类,再构造派生类,而在构造基类的时候,它其实就是一个基类,假如我们在基类的构造函数里面调用了一个虚函数,那其实执行的就是基类实现的这个虚函数,而不会是派生类重写的版本。(析构同理)

C++纯虚函数


 一、定义

  virtual void funtion()=0 

(纯虚函数就是类里面没有定义的一个虚函数,实现一个纯虚函数的方法就是在虚函数的声明后面加个=0,然后有纯虚函数的类叫做抽象基类,我们不能生成一个抽象基类的对象,抽象基类的最大作用就是定义了一个接口类,我的理解)
二、引入原因
   1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 
   2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
三、相似概念
   1、多态性 
  指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。 
  a、编译时多态性:通过重载函数实现 
  b、运行时多态性:通过虚函数实现。 
  2、虚函数 
  虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
  3、抽象类 
  包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
 

虚表指针,虚拟继承,多重继承

单继承下的虚函数:每个类对象里面只会有一个虚表指针,这个类的所有对象都指向同一个虚函数表。(虚表指针可以放在最前面,也可以放在最后面,放在最前面为了多重继承下,去调用虚函数更方便,而放在最后面,可以兼容C)

多重继承下的虚函数:会为每一条继承线路维护一个虚函数表(也就是有n个虚表指针)。

虚函数表分为主要实例,和次要实例,第一顺位的基类子对象里面存的vptr指向的虚函数表,存的是派生类的所有虚函数。而其他顺位基类子对象的vptr指向的虚函数表,存的只是跟它有关系的(也就是它原来就有的)虚函数。

多重继承的问题:https://blog.csdn.net/speargod/article/details/101075037

虚拟继承下的虚函数:一个虚拟基类. 主流方案是将虚拟基类作为共享部分, 其他类通过指针指向虚拟基类对象或者在虚函数表里面存这个虚基类对象在对象中的偏移量  ,来获得虚拟基类的地址. gcc的做法是将虚基类放在对象末尾, 在虚表中添加一项, 记录虚基类对象在对象中的偏移, 从而获得其地址.。

struct和class区别

没什么特别区别吧。

主要就是:

  • class默认成员是私有的private,struct默认成员是公有的public
  • class继承默认是私有的private,struct继承默认是公有的public(这个派生类是class,那么它默认继承基类的方式是private继承)

空类(没有数据成员)

其大小是1个字节,编译器会塞给它一个char,为了让这个类的每个对象都有唯一的地址。

C++ 空类,默认产生哪些成员函数

默认构造函数、拷贝构造函数、析构函数、拷贝赋值运算符 这四个是我们通常大都知道的。但是除了这四个,还有两个,那就是取址运算符和 取址运算符 const。

https://www.cnblogs.com/timesdaughter/p/6684633.html

6种构造函数的写法: https://blog.csdn.net/speargod/article/details/86817460

内存对齐

是什么?

对象存储的内存地址,不能乱放,必须是某个值的整数倍。还有就是假如是一个类对象的话,它内部的数据成员,也不是简单的一个挨着一个这样存储的,会有一些空白位置存在(也就是内部碎片)

这个值跟编译器,操作系统有关系吧,在32平台下默认好像是4字节对齐。(可以通过预编译指令#pragma pack(n)改变)

结构体对齐规则:

#pragma pack(n)

1. 起始位置必须满足4字节对齐要求

2. 之后的每个数据成员相对于起始位置的偏移量必须是默认对齐系数n和该变量类型大小,两者较小值的整数倍,不是整数倍前面空出内存,直到偏移量是整数倍为止。

3. 结构体的整体大小要是n和最大变量大小中较小值的整数倍,不足的最后补空。

4. 假如嵌套的结构体,也要让它满足这个要求(最好别说,因为不确定)

e.g.:

#pragma pack(4)

struct AA {

int a;       //偏移量为0,ok,存放位置区间[0,3]

char b;  //长度1 < 4 按1对齐;偏移量为4;存放位置区间[4]

short c;     //长度2 < 4 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]

};     总的大小为8,是4的倍数,所以没有问题。

原因:

一句话:为了可移植性和提高处理器的性能。

详细:

1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器可能需要作两次内存访问;而对齐的内存访问仅需要一次访问。比如假如这个处理器只能访问偶数的内存,然后一次只能读4个字节,那么如果我一个int放在了奇数位置上,那么只能读两次,然后拼接起来。

红黑树与avl树

avl树定义:AVL树是严格的平衡二叉树,所有节点的左右子树高度差都不能不超过1。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而它的旋转非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况。

红黑树: 是一个弱平衡二叉查找树,它的主要性质是

  • 节点为红色或者黑色;
  • 根节点为黑色,叶子节点为黑色;
  • 从每个节点到叶子节点,路径上包含相同数量的黑色节点;
  • 如果一个节点为红色,那么两个儿子节点就得是黑色。

红黑树确保没有一条路径会比其它路径长出两倍,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,用红黑树比较好。

红黑树的search,insert,delete操作时间复杂度都是O(lgn)。

C++四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast

  • const_cast用于将const变量转为非const
  • static_cast用的最多,对于各种隐式转换,非const转const,void*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
  • dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
  • reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
  • 为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
     

dynamic_cast<type* > (a)  dynamic_cast<type&> (a)

函数指针

函数指针就是指向函数的指针变量。

可以用来调用函数和做函数的参数,就是回调函数。

要想声明一个指向该类函数的指针,只需用指针替换函数名即可

  1. int (*pf)(int,int);//未初始化  

static

存储位置、生存期、作用域、  属于类还是属于对象

https://blog.csdn.net/speargod/article/details/85717317

++i,和i++ 重载

++i  T& operator++();

i++  T operator++ (int);

前置版本没有参数,并且为了与内置类型一致,返回的是T &

后置版本有个int 参数,并且返回的是 T ,而不是引用,也是为了与内置类型一致。

T& operator++(){
	*this +=1;
	return *this;
}

T operator++ (int){
	int res=*this;
	*this += 1;
	return res;
}

手写memcpy,strcpy,memset,strcat,strcmp

https://blog.csdn.net/speargod/article/details/100087823

手写单例

https://blog.csdn.net/speargod/article/details/100083056

重载、隐藏、覆盖

https://blog.csdn.net/speargod/article/details/86631533

stl容器是否线程安全?

不是,为了效率,没有给所有操作加锁,所以如果同时读写的话,需要自己加锁

在main函数执行前运行

在C++中,可以利用全局变量和构造函数的特性,通过全局变量的构造函数在main()函数之前执行

class BeforeMain{
public:
 BeforeMain();
};

BeforeMain::BeforeMain() {
 cout << "Before main" << endl;
}

BeforeMain bM; // 利用全局变量和构造函数的特性,通过全局变量的构造函数执行

C++内存管理

CSAPP p13  

牛客编译与底层 4  https://www.nowcoder.com/tutorial/93/8f140fa03c084299a77459dc4be31c95

https://www.cnblogs.com/mrlsx/p/5411874.html

c++把一个进程的虚拟地址空间分为5个区,从下到上依次是: 代码区(放二进制代码),data区(已经初始化了全局变量或者静态变量,还有只读的字符串常量),bss区(未初始化的全局变量或者静态变量),堆区(向上增长,调用new/malloc函数分配的变量),栈区(向下增长,局部变量,函数参数,返回地址)

new/malloc 的区别

1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配; malloc(sizeof int);

2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。

3、new不仅分配一段内存,而且会调用构造函数,malloc不会。

4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。

5、new是一个操作符,而malloc是C的库函数。new 实际做了两件事 1. 调用operator new分配内存。 2. 调用构造函数初始化对象。而我们可以重载operator new。delete做了两件事 1. 调用析构函数清理对象。 2. 调用operator delete释放空间。

6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。

7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。

8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

malloc的实现:

Malloc函数是用于在堆区分配内存。而堆区的管理是类似于内存池的方式,先申请大块内存作为堆区,然后用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块,以块作为内存管理的基本单位。而未分配块又是用显式双向链表结构来管理的,也就是内部存了两个指针,分别指向上一个空闲块,和下一个空闲块。那malloc的实现就是,它先去这个堆区搜索满足要求的空闲块,也就是大小大于要找的size的空闲块,找到了就把这个空闲块给切下来,返回起始位置的地址,搜索的策略可能是首次适配(从头开始找)、下一次适配(从上一次查询结束的位置开始找)、最佳适配(找符合要求的最小空闲块),而假如找不到,就用brk或者mmap系统调用,再申请一块内存给堆区。假如还是失败了,那就返回null。

所以假如是在堆区的地址越界,是很难发现的,因为可能你访问的地址空间,仍然处于堆区,在系统看来是合法,所以可能会在程序运行很久之后才出现问题。当然,你如果越界的地址超过了brk(一个系统变量,指向堆区顶部)那程序直接终止。

栈区的地址越界同理,不过因为栈区放变量是一个挨着一个,并且最大大小也比较小,所以地址越界导致程序终止的可能性会更大一点。

Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk把堆区给拔高;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

https://blog.csdn.net/edonlii/article/details/22601043

以下四行代码的区别是什么?

const char * arr = "123";

char * brr = "123";

const char crr[] = "123";

char drr[] = "123";

 

1. 声明指向串“123”的一个常量字符指针

2. 编译错误,“123”是const char* 类型的指针,不能用来初始化char* 指针

3. 初始化一个常量字符数组,相当于const char crr1[4] = { '1','2','3' };  (要特别注意字符数组的长度)

既然是常量数组,那么再对它执行下列操作就是错误的了

   crr[4] = "12";   (表达式必须是可以被修改的左值)

4. 声明一个指向“123”的字符数组:

举例说明一些问题:

    char * ptr;  drr = ptr;      // 错误,drr是不可以被修改的值

    drr [5]= (char)"1234";    // 正确,将“1234”  const char * 类型强制转化为char类型
    drr[5] = "1234";     // 不能将const char *类型分配到char类型的 实体上

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值