北京建站公司排名首推万维科技系统优化软件推荐
背景:汇总了网上C++常考的基础知识,方便复习
1,static关键字
static可以用于成员变量,或者成员函数。存储空间在静态存储区(编译器会将其初始化为0,对应的存储空间直到程序执行结束才会释放),作用于声明的文件中。静态定义的变量或者函数可以通过类名或者对象调用。
普通定义和加static关键字定义的区别:
- 普通成员变量每个对象各自一份,静态成员变量一共就一份,为所有对象共享
- 普通成员变量只能通过创建对象后调用,而静态成员不需要通过对象就能访问,通过class::static_val_name直接访问
- 普通成员函数必须作用于某个对象(必须创建对象),静态成员函数可直接通过class::static_func_name访问
- static修饰全局变量时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即使extern外部声明
- static成员只能在类外初始化
为什么引入static?
函数内部变量的定义,编译器为其在栈上分配空间,函数执行结束就会释放,如果需要将函数中变量的值保存至下一次调用,如果定义全局变量会破坏此变量的访问范围。加入static关键字则可解决此问题。
在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其加入static关键字定义。
2,extern关键字
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
extern是c++引入的一个关键字,它可以应用于一个全局变量,函数或模板声明,说明该符号具有外部链接(external linkage)属性。也就是说,这个符号在别处定义。一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译。于是就需要在文件间共享数据,这里extern就发挥了作用。
C++中的链接属性
链接属性一定程度范围决定着符号的作用域,C++中链接属性有三种:none(无)、external(外部)和 internal(内部)。
- external,外部链接属性。非常量全局变量和自由函数(除成员函数以外的函数)均默认为外部链接的,它们具有全局可见性,在全局范围不允许重名,详情可见例子。
- internal,内部链接属性。具有该属性的类型有,const对象,constexpr对象,命令空间内的静态对象(static objects in namespace scope)
- none,在类中、函数体和代码块中声明的变量默认是具有none链接属性。它和internal一样只在当前作用域可见。
extern有3种用法
- 非常量全局变量的外部链接(全局非常量的变量默认是外部链接的)
- 常量全局变量的外部链接(常量全局变量默认是内部链接的,所以想要在文件间传递常量全局变量需要在定义时指明extern)
- extern "C" 和extern "C++"函数声明
使用extern和包含头文件来引用函数有什么区别呢?
与include相比,extern引用另一个文件的范围小,include可以引用另一个文件的全部内容。extern的引用方式比包含头文件要更简洁。extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。
3,头文件中的ifndef/define/endif和program once
相同点:防止头文件被重复包含 ,即头文件只被定义一次。
不同点:
- 1). ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
- 2). 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。
- 3). ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
- 4). 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特别需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。
4,const常量,和#define的区别
1)编译器处理方式
define – 在预处理阶段进行替换
const – 在编译时确定其值
2)类型检查
define – 无类型,不进行类型安全检查,可能会产生意想不到的错误
const – 有数据类型,编译时会进行类型检查
3)内存空间
define – 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大
const – 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
4)其他
在编译时, 编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
宏替换只作替换,不做计算,不做表达式求解。
5,sizeof和strlen的区别
- sizeof 是一个操作符,strlen 是库函数。
- sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\ 0‘的字符串作参数。
- 编译器在编译时就计算出了sizeof 的结果。而strlen 函数必须在运行时才能计算出来。并且 sizeof计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
- 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
6,指针和引用的区别
从概念上讲,指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
- 引用必须被初始化,但是不分配存储空间。指针声明时可以不初始化,在初始化的时候需要分配存储空间。
- 引用初始化后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
- 指针是一个实体,而引用仅是个别名;
- 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
- 引用没有const,指针有const,const的指针不可变;
- 引用不能为空,指针可以为空;
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
- 指针和引用的自增(++)运算意义不一样;
- 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
7,封装、继承和多态
面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!
1)封装:封装可以隐藏实现细节,使得代码模块化;封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。
2)继承:继承是指这样一种能力,它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程。
通过继承创建的类称为子类或者派生类,被继承的类叫做父类或者基类。一个子类可以继承多个基类。但是一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。继承概念的实现方式有三类:实现继承、接口继承和可视继承。
- 虚函数:继承接口和实现。派生类可以override实现,可以使用base版本。
- 纯虚函数:只继承接口,派生类必须提供其实现接口。(如派生类各有不同的实现标准时候)
- 非虚函数:继承接口和实现。派生类不可以override。
为什么不能把所有函数设置为虚函数?虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候都会产生一定的系统开销,这是没有必要的。
class CShape{
public:virtual void draw()=0; //=0表示纯虚函数,在派生类中实现virtual void setColor(const Color& color); //虚函数,可在基类定义一个默认实现,在派生类中可重写void CommonFunciton(); //普通函数
private:Color m_color;
};class CCircle:public CShape{};
class CEllipse:public CShape{};
3)多态:多态允许将子类类型的指针赋值给父类类型的指针,可以简单地概括为“一个接口,多种方法”,程序在运行时才决定要调用的函数。多态有两种实现方式,覆盖和重载。
C++的多态性是通过虚函数来实现的,虚函数允许派生类重新定义成员函数,而派生类重新定义基类的做法称为覆盖,或者称为重写。(重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)
而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。
8,内存泄露的概念
内存泄露一般指堆内存的泄露。用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
- 使用的时候要记得指针的长度.
- malloc的时候得确定在那里free.
- 对指针赋值的时候应该注意被赋值指针需要不需要释放.
- 动态分配内存的指针最好不要再次赋值.
- 在C++中应该优先考虑使用智能指针.
9,智能指针
简答:智能指针是C++11引入的类模板,用于管理资源,行为类似于指针,但不需要手动申请、释放资源,所以称为智能指针。
在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
智能指针是为了帮助程序员管理动态分配的内存,可以帮助我们自动释放new出来的内存,从而避免内存泄漏。C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针括 std::shared_ptr
/std::unique_ptr
/std::weak_ptr
,使用它们需要包含头文件 <memory>
。
这三种智能指针的特征及用途:
shared_ptr
使用了引用计数(use count
)技术,当复制个shared_ptr
对象时,被管理的资源并没有被复制,而是增加了引用计数。当析构一个shared_ptr
对象时,也不会直接释放被管理的的资源,而是将引用计数减一。当引用计数为0时,才会真正的释放资源。shared_ptr
可以方便的共享资源而不必创建多个资源。
unique_ptr
则不同。unique_ptr
独占资源,不能拷贝,只能移动。移动过后的unique_ptr
实例不再占有资源。当unique_ptr
被析构时,会释放所持有的资源。
weak_ptr
可以解决shared_ptr
所持有的资源循环引用问题。weak_ptr
在指向shared_ptr
时,并不会增加shared_ptr
的引用计数。所以weak_ptr
并不知道shared_ptr
所持有的资源是否已经被释放。这就要求在使用weak_ptr
获取shared_ptr
时需要判断shared_ptr
是否有效。
C++面试八股文:什么是智能指针? - 掘金
第 5 章 智能指针与内存管理 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly
10,宏和内联(inline)函数的比较
- 首先宏是C中引入的一种预处理功能;
- 内联(inline)函数是C++中引入的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
- 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
- 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
- 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译器决定(当然可以通过设置编译器,强制使用内联);
- 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
- 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。
11,malloc/free和new/delete
1) malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2) 对于非内部数据类型(自定义类型)的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
最后补充一点题外话,new 在申请内存的时候就可以初始化(如下代码), 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。
12,构造函数和析构函数的执行顺序
构造函数
- 首先调用父类的构造函数;
- 调用成员变量的构造函数;
- 调用类自身的构造函数。
对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。
13,对C/C++内存的了解
在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。
静态内存分配:编译时分配。包括:全局、静态全局、静态局部三种变量。
动态内存分配:运行时分配。包括:栈(stack): 局部变量。堆(heap): c语言中用到的变量被动态的分配在内存中(malloc或calloc、realloc、free函数)。
变量的内存分配
栈区(stack):指那些由编译器在需要的时候分配,不需要时自动清除的变量所在的储存区,如函数执行时,函数的形参以及函数内的局部变量分配在栈区,函数运行结束后,形参和局部变量去栈(自动释放)。栈内存分配运算内置与处理器的指令集中,效率高但是分配的内存空间有限。
堆区(heap):指哪些由程序员手动分配释放的储存区,如果程序员不释放这块内存,内存将一直被占用,直到程序运行结束由系统自动收回,c语言中使用malloc,free申请和释放空间。
静态储存区(static):全局变量和静态变量的储存是放在一块的,其中初始化的全局变量和静态变量在一个区域,这块空间当程序运行结束后由系统释放。
常量储存区(const):常量字符串就是储存在这里的,如“ABC”字符串就储存在常量区,储存在常量区的只读不可写。const修饰的全局变量也储存在常量区,const修饰的局部变量依然在栈上。
程序代码区:存放源程序的二进制代码。
14,C++11后新特性
- auto类型推导
- 范围for循环
- lambda函数
- override 和 final 关键字
- 空指针常量nullptr
下面是override和final关键字的使用场景
/*如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:*/
class A
{virtual void foo();
}
class B :public A
{void foo(); //OKvirtual foo(); // OKvoid foo() override; //OK
}class A
{virtual void foo();
};
class B :A
{virtual void f00(); //OKvirtual void f0o()override; //Error
};
14,悬空指针和野指针
悬空指针:当所指的对象被释放或者收回,但是没有让指针指向NULL
野指针:未初始化的指针。悬空指针是野指针的子集。
C语言中的“悬空指针”会引发不可预知的错误,而且这种错误一旦发生,很难定位。这是因为在 free(p) 之后,p 指针仍然指向之前分配的内存,如果这块内存暂时可以被程序访问并且不会造成冲突,那么之后使用 p 并不会引发错误。
因为“野指针”可能指向任意内存段,因此它可能会损坏正常的数据,也有可能引发其他未知错误,所以C语言中的“野指针”危害性甚至比“悬空指针”还要严重。在实际的C语言程序开发中,定义指针时,一般都要尽量避免“野指针”的出现(赋初值)
示例:
#include<stdio.h>
int main()
{//C语言风格int* p1 = NULL;//初始化指针int* p1 = (int*)malloc(sizeof(int) * 100);//C语言风格malloc对应free;free(p1); //C语言风格p1 = NULL;//如果没有这一步置空操作,那么就会发生野指针;//C++风格int* p2 = NULL;//初始化指针p2 = new int[100];//C++风格new对应deletedelete[]p2; //C++风格p2 = NULL; //如果没有这一步置空操作,那么就会发生野指针;system("pause");return 0;
}
当指针指向某个对象之后,当这个对象的生命周期已经结束时,对象已经销毁之后,如果仍使用指针访问该对象,这时就会出现错误,造成野指针。
#include<stdio.h>
int maain()
{int* p1;printf("%p\n", p1);//编译可以通过,但是在运行的时候会出错return 0;
}
15,struct和class指针
本质区别是默认的继承访问权限:class是private, struct是public
16,C++中vector和list的区别
vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。
list数据结构
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。
vector和list的区别
vector拥有一段连续的内存空间,能很好的支持随机存取,
因此vector<int>::iterator支持“+”,“+=”,“<”等操作符。
list的内存空间可以是不连续,它不支持随机访问,
因此list<int>::iterator则不支持“+”、“+=”、“<”等
vector<int>::iterator和list<int>::iterator都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。
17,重载和覆盖的区别
虚函数是基类希望派生类重新定义的函数,派生类重新定义基类虚函数的做法叫做覆盖;
重载就在允许在相同作用域中存在多个同名的函数,这些函数的参数表不同。重载的概念不属于面向对象编程,编译器根据函数不同的形参表对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。
重载的确定是在编译时确定,是静态的;虚函数则是在运行时动态确定。
18,进程和线程的区别
- 进程是程序的一次执行,线程是进程中的执行单元;
- 进程间是独立的,这表现在内存空间、上下文环境上,线程运行在进程中;
- 一般来讲,进程无法突破进程边界存取其他进程内的存储空间;而同一进程所产生的线程共享内存空间;
- 同一进程中的两段代码不能同时执行,除非引入多线程。
19,sprintf, strcpy, memcpy函数的区别
这些函数的区别在于 实现功能以及操作对象不同。
(1)strcpy 函数操作的对象是字符串,完成从源字符串到目的字符串的拷贝功能。
(2)sprintf 函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。
(3)memcpy函数顾名思义就是内存拷贝,实现将一个内存块的内容复制到另一个内存块这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy的操作对象不局限于某一类数据类型,或者说可适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于memcpy函数等长拷贝的特点以及数据类型代表的物理意义,memcpy函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。
对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:
- strcpy 无疑是最合适的选择:效率高且调用方便。
- sprintf 要额外指定格式符并且进行格式转化,麻烦且效率不高。
- memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 +1),还会带来性能的下降。其实 strcpy 函数一般是在内部调用 memcpy 函数或者用汇编直接实现的,以达到高效的目的。因此,使用memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。
20,虚函数可以被声明为static吗?
不可以,因为静态成员函数/变量没有this指针。