为有机会进大厂,程序员面试必须掌握的C++知识点

原创不易,如果需要转载请附上本连接

一、C++知识点

1. const 关键字的使用场景

关键词const表示所修饰的变量或指针是常量。const 可以用来定义全局变量、局部变量;被const修饰的变量效果一致,只能在初始化时候赋值。

// 1、常量变量
const int a = 3; //初始化时候必须赋值
a = 5; //编译时候会报错

// 2、常量指针(指针指向的值不能修改)
const int *a = NULL; //等同于 int const *a = NULL;
int b = 3;
a = &b; //正确
*a = 6; //编译时候会报错

// 3、指针常量(指针地址不能修改)
int* const a = NULL;
int b = 10;
*a = 6; //正确
a = &b; //编译报错

// 4、如何区分指针常量和常量指针
前提:把const 当做常量,*当作指针
那么:
const * :常量指针
* const :指针常量

c++ 中定义成员函数的时候后面加const。其作用是该函数体内不允许修改类的成员变量。

2. static 关键字的使用场景

  1. 函数体内作用范围为该函数体,该变量内存只被分配一次,具有记忆能力(内存分配在静态区,在第一次调用的时候分配内存,函数调用结束内存并不释放)
  2. 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;(模块,{}括起来的语句块都是,不同的文件也是不同的模块)
  3. 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  4. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
  5. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

static关键字的四种使用场景

#include<iostream>
using namespace std;
static int i = 5;  // 1、静态全局变量 :限制作用域是当前文件下 其他文件extern int i 不可用
static void fun1() //3、静态函数 : 静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。 
{
    /*code*/
}
void fun2()
{
    int i = 1; 
    static int j = 2; // 2、静态局部变量 :具有全局寿命,局部可见,只第一次进入函数时被初始化
}
int main()
{
    /*code*/
}
class A{
public:
    static void fun(A a);
private:
    static int x;
};
void A :: fun(A a){
    cout << a.x; 
}
int A :: x= 0;

C++ 中的static关键字使用场景


3. explicit 关键字的使用场景

  1. explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了

  2. 声明为explicit的构造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象。

    	Base base(‘a’) //显示调用,OK
    	Base base = ‘a’ //隐是调用,err
    

4. volatile 关键字的使用场景

  • 编译器的优化方式有:将内存变量缓存到寄存器,由于访问寄存器要比访问内存单元快的多。
  • 有时编译器对代码会自动进行优化,该关键字就是让编译器不要进行编译优化。volatile意思是“易变的”“直接存取原始内存地址”。(防止变量改变后被编译器优化一直只读寄存器第一次的值,不变了)
  • 常用地点
    1. 中断服务程序中修改的供其它程序检测的变量,需要加volatile;(编译器判断主函数里没有改变该变量,就可能把他优化了,那就拿不到改变后的变量值了)
    2. 多任务环境下各任务间共享的标志,应该加volatile; (如多线程)
    3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;(编译器可能认为中间一些操作是无意义的,直接采用最后操作的结果,但是可能对某个设备初始化正是要中间的过程)

关于 volatile 关键字的应用场景

public static void main(String[] args) throws InterruptedException {
	class Thread1 extends Thread{
		private volatile boolean stopped;
		public void run(){
			while(!stopped){
				System.out.println("running");
			}
				System.out.println("stop");
		}
		void stopT(){
			stopped = true;
		}
	}
	Thread1 t1 = new Thread1();
	t1.start();
	Thread.sleep(1000);
	t1.stopT();
}
// 子线程t1需要根据主线程执行情况进行停止,只要能搞读取到stopped变量值即可,volatile修饰后的值,正好能够从内存中读取该变量,完美解决子线程停止问题。

5. 什么是封装、继承

封装和继承:封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面,他把实现的细节影藏起来了,比如你在java中去实现一个类,这个类中提供了一些功能方法,你只需要知道你需要传递什么样的参数,会达到什么样的效果,实现细节在类中定义好了。从而使得代码模块化;而继承可以扩展已存在的代码模块,而目的就是为了代码重用。


6. 什么是多态

用最简单的一句话就是:父类型的引用指向子类型的对象。用一句比较通俗的话:同一操作作用于不同的对象,可以产生不同的效果。这就是多态。

多态的两种类型:

  1. 静态多态性:包括变量的隐藏、方法的重载(指同一个类中,方法名相同[方便记忆],但是方法的参数类型、个数、次序不同,本质上是多个不同的方法)
  2. 动态多态性:是指子类在继承父类(或实现接口)时重写了父类(或接口)的方法,程序中用父类(或接口)引用去指向子类的具体实例,从代码形式上看是父类(或接口)引用去调用父类(接口)的方法,但是在实际运行时,JVM能够根据父类(或接口)引用所指的具体子类,去调用对应子类的方法,从而表现为不同子类对象有多种不同的形态。

多态有什么好处?

  • 有两个好处:
  1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
  2. 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,

7. 虚函数的实现原理

带有虚函数的类,编译器会为其额外分配一个虚函数表,里面记录的使虚函数的地址,当此类被继承时,子类如果也写了虚函数就在子类的虚函数表中将父类的函数地址覆盖,否则继承父类的虚函数地址。

实例化之后,对象有一个虚函数指针,虚函数指针指向虚函数表,这样程序运行的时候,通过虚函数指针找到的虚函数表就是根据对象的类型来指向的了。虚函数的实现原理


8. 构造函数可以是虚函数吗, 为什么?

虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。


9. 析构函数可以是虚函数吗为什么?有什么应用场景

首先析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则可能存在内存泄露的问题。

应用场景,举例:
派生类B 继承 基类A;

 A *p = new B; 
 delete p;
  1. 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
  2. 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。

补充:

B *p = new B; 
delete p;

这样操作也是先调用B的析构函数,再调用A的析构函数。


10. 智能指针有哪些,实现原理以及用法

先来说一下四种常用的智能指针,我按使用度从低到高排一下顺序,分别是:auto_ptr、unique_ptr、shared_ptr、 weak_ptr。

  1. auto_ptr 最显著的特点就是一个对象的空间只能一个对象用,不可以两个对象共用同一块空间,避免了程序崩溃问题,当我们赋值以后我们以前的对象资源就被置空了。
  2. unique_ptr 主要的特点是我们不能进行赋值,拷贝,而我们实现也和auot_ptr简单的实现原理差不多的,主要是拷贝,赋值函数的私有化,并且在c++98里面我们只声明不定义。
  3. shared_ptr是通过引用计数的方法管理同一块内存的,这样内存什么时候释放,内存指向会不会成为野指针就知道了。(在线程安全问题)
  4. weak_ptr 和 shared_ptr 两个智能指针类都公有继承了一个抽象的引用计数的类,所以,shared_ptr和weak_ptr的实现方式所差无几,就是二者的引用计数有区别。(双端队列节点指针)

四种智能指针的用法和原理


11. 什么是模板特化

模板特化(template specialization) 不同于模板的实例化,模板参数在某种特定类型下的具体实现称为模板特化。模板特化有时也称之为模板的具体化。

分别有函数模板特化类模板特化
函数模板特化:函数模板特化指函数模板在模板参数为特定类型下的特定实现。
类模板特化:类模板特化类似于函数模板的特化,即类模板参数在某种特定类型下的具体实现。

模板特化


12. new 和 malloc 区别

  1. 申请的内存所在位置

    new 操作符从自由存储区(free store)上为对象动态分配内存空间(内存即为自由存储区包括堆、静态存储区等具体看new实现)。

    malloc 函数从堆上动态分配内存。

  2. 返回类型安全性

    new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故 new 是符合类型安全性的操作符。

    malloc 内存分配成功则是返回 void * ,需要通过强制类型转换将 void* 指针转换成我们需要的类型。

  3. 内存分配失败时的返回值

    new 内存分配失败时,会抛出bac_alloc异常,它不会返回NULL。

    malloc 分配内存失败时返回NULL。

  4. 是否需要指定内存大小
    new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。

    malloc 则需要显式地指出所需内存的尺寸。

  5. 是否调用构造函数/析构函数
    new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。

    malloc则不会。

  6. 对数组的处理
    C++ 提供了new[] 与 delete[] 来专门处理数组类型,使用 new[] 分配的内存必须使用 delete[] 进行释放

    至于malloc需要我们手动自定数组的大小

  7. new与malloc是否可以相互调用
    operator new /operator delete 的实现可以基于malloc,而malloc的实现不可以去调用new。

  8. 是否可以被重载
    opeartor new /operator delete 可以被重载。

    malloc/free 并不允许重载。

  9. 能够直观地重新分配内存
    malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。

    new 没有这样直观的配套设施来扩充内存。

  10. 客户处理内存分配不足

new与malloc有什么区别


13. C++ 内存空间布局

内存布局

  1. 代码区(text segment):又称只读区。通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,比如字符串常量等。
  2. 全局初始化数据区/静态数据区(Data Segment):用来存放程序已经初始化的全局变量,已经初始化的静态变量。位置位于可执行代码段后面,可以是不相连的。在程序运行之初就为数据段申请了空间,程序退出的时候释放空间,其生命周期是整个程序的运行时期。
  3. 未初始化数据区(BSS):用来存放程序中未初始化的全局变量和静态变量,位置在数据段之后,可以不相连。其生命周期和数据段一样。(2)和(3)统称为静态存储区。
  4. 栈区(Stack):又称堆栈,存放程序临时创建的局部变量,如函数的参数值、返回值、局部变量等。也就是我们函数括弧{}中定义的变量(但不包括static声明的静态变量,static意味着在数据段中存放的变量)。除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等到调用结束后,函数的返回值也会被存放回栈中。编译器自动分配释放,是向下有限扩展的。
  5. 堆区(Heap):位于栈区的下面,是向上有限扩展的。用于存放进程运行中动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存的时候,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存的时候,被释放的内存从堆中被剔除(堆被缩减)。一般由程序员进行分配和释放,若不释放,在程序结束的时候,由OS负责回收。

const修饰的全局变量保存在代码区中,const修饰的局部变量保存在栈段中。


14.如何限制对象只能在堆上创建

  1. 动态建立类对象,是使用 new 运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行 operator new() 函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

  2. 将析构函数设为私有,类对象就无法建立在栈上了


15.如何限制对象只能在栈上创建

  1. 静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

  2. 只有使用 new 运算符,对象才会建立在堆上,因此,只要禁用 new 运算符就可以实现类对象只能建立在栈上。

如何限制对象只能建立在堆上或者栈上


16.如何让类不能被继承

  1. 将类的构造函数定义为带private属性。
  2. 将该类虚继承一个父类,但是该父类的构造函数是带private属性的。

C++ 友元函数
实现一个无法被继承的C++类


17.什么是单例模式,工厂模式

  1. 单例模式: 单例模式它限制了类的实例化次数只能一次。在实例不存在的情况下,可以通过一个方法创建一个类来实现创建类的新实例;如果实例已经存在,它会简单返回该对象的引用。

  2. 抽象工厂(Abstract Factory)模式: 工厂模式提供一个通用的接口来创建对象。

单例模式与工厂模式


18.C++ auto 类型推导的原理

auto 的推导规则
它可以同指针、引用结合起来使用,还可以带上 cv 限定符(cv-qualifier,const 和 volatile 限定符的统称)。
两条规则:

  1. 当不声明为指针或引用时,auto 的推导结果和初始化表达式抛弃引用和 cv 限定符后类型一致。
  2. 当声明为指针或引用时,auto 的推导结果将保持初始化表达式的 cv 属性。

注意: auto 是不能用于函数参数的。

理解auto类型推断
C++ auto(类型推导)精讲
C++ auto类型推导完全攻略


19.泛型编程如何实现的

泛型编程最初诞生于C++中,由Alexander Stepanov[2]和David Musser[3]创立。目的是为了实现C++的STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。比如qsort泛化之后就变成了:

template<class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first, RandomAccessIterator last,
        Compare comp);

其中first,last这一对迭代器代表一个前闭后开区间,迭代器和前开后闭区间都是STL的核心概念。

泛型编程:源起、实现与意义


20.指针和引用的区别

本质:引用是别名,指针是地址。

具体的:

  1. 从现象上看,指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。

  2. 从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域,因为引用声明时必须初始化,从而指向一个已经存在的对象。引用不能指向空值。
    注:标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体随编译器
    引用会占用内存空间吗?

  3. 从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针。

  4. 不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。

  5. 理论上,对于指针的级数没有限制,但是引用只能是一级。如下:

      int** p1;         // 合法。指向指针的指针
      int*& p2;         // 合法。指向指针的引用
      int&* p3;         // 非法。指向引用的指针是非法的
      int&& p4;         // 非法。指向引用的引用是非法的
    

    注意上述读法是从左到右。

浅谈C/C++引用和指针的联系和区别


21.指向派生类的基类指针

#include <iostream>
using namespace std;
class CBase
{
protected:
    int n;
public:
    CBase(int i) :n(i) { }
    void Print() { cout << "CBase:n=" << n << endl; }
};
class CDerived :public CBase
{
public:
    int v;
    CDerived(int i) :CBase(i), v(2 * i) { }
    void Func() { };
    void Print()
    {
        cout << "CDerived:n=" << n << " CDerived:v=" << v << endl;
    }
};
int main()
{
    CDerived objDerived(3);
    CBase objBase(5);
    CBase * pBase = &objDerived; // 使得基类指针指向派生类对象
                                 //pBase->Func(); //错, CBase类没有Func()成员函数
                                 //pBase->v = 5;  //错 CBase类没有v成员变量
    pBase->Print();
    cout << "1)------------" << endl;	//CDerived * pDerived = & objBase; //错,不能将基类指针赋值给派生类指针
    CDerived * pDerived = (CDerived *)(&objBase);
    pDerived->Print();  //慎用,可能出现不可预期的错误
    cout << "2)------------" << endl;
    objDerived.Print();
    cout << "3)------------" << endl;
    pDerived->v = 128;  //往别人的空间里写入数据,会有问题
    objDerived.Print();
    return 0;
}
CBase:n=3
1)------------
CDerived:n=5 CDerived:v=5
2)------------
CDerived:n=3 CDerived:v=6
3)------------
CDerived:n=128 CDerived:v=6

指向派生类的基类指针和基类引用的访问范围:

  1. 指向派生类的基类指针或者引用,其类型仍然属于基类类型,而不是派生类类型,尽管它指向的是派生类。其访问范围受其基类类型影响,因此只能访问基类中可以访问的类型。
  2. 对于虚函数,使用指向派生类的基类指针或基类引用访问时,将会体现出多态性,调用的是实际上是派生类的对应函数。
  3. 对于指向派生类的基类引用,虽然说引用通常是被引用对象的一个别名,但这里,基类引用的访问范围与被引用对象的访问范围明显是不一样的!

一句话这样总结:当基类类型的指针或引用使用派生类的对象时,它对虚函数的调用,实际上是调用了被指向对象类的函数。注意,取决于被指向对象。
当它对非虚函数调用时,会使用基类自身的函数。

C++基类和派生类指针的相互赋值和转换
C++中指向派生类的基类指针


  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值