C/C++基础知识点面试题

目录

一、虚函数的数据结构,如何工作?

二、const与define的区别?

三、指针与引用的区别?

四、指针与数据的区别?

五、不用临时变量实现两个变量的交换

七、一个C++源文件从文本到可执行文件经历的过程

八、C++11新特性

九、C++和C的不同

十、malloc的原理

十一、内存泄漏、野指针

十二、static

十三、union和struct

十四、new与malloc的区别

十五、C++类型转换

十六、面向对象的了解

十七、前置和后置的区别

十八、静态库和动态库

十九、struct内存大小的确定

二十、strlen,strcpy函数的实现

二十一、memcpy,memset内部函数实现

二十二、C/C++内存管理方式,内存分配

二十三、深拷贝和浅拷贝

二十四、debug和release的区别

二十五、main是否需要返回值?

二十六、C++动态链接库与C动态链接库

二十七、结构体

二十八、拷贝构造函数为什么传引用?

二十九、程序崩溃原因

三十、C++字符串输入

三十一、lambda表达式

一、虚函数的数据结构,如何工作?

虚函数:用virtual定义的函数为虚函数。

虚函数来源:基于C++的一个特性:子类能转换成父类,例如: 

CBasic    *parent;
CBasic    *p1;
CChildren *child;

parent = new CBsic;
 child = new CChildren;
    p1 = new CChildren;

如上代码,p1为CBasic型指针,但实际对象是CChildren型,如果子类和父类有相同的函数时,是调用子类的函数还是父类的函数?基于这个问题,C++提出多态的概念:根据实际对象类型决定函数调用的具体目标,使用virtual关键字对多态进行支持。被virtual声明的函数被重写后具有多态性。

底层机制:虚函数是使用虚函数表和虚函数表指针实现的。虚函数表是一个类虚函数的地址,用于索引类本身和类虚函数,若子类重写父类函数,则会在相应的虚函数表处替换成子类虚函数的地址。虚函数表指针存在于每一个对象中,它指向对象所对应类的虚函数地址。

构造函数是不是虚函数并无多大影响,因为在构造子类一定要先构造父类。在存在继承并且析构函数需要用来析构资源时,析构函数一定要为虚函数,若使用父类指针指向子类,用delete析构函数时,只会调用父类析构函数,不会调用子类的析构函数,造成内存泄漏。

二、const与define的区别?

1、编译器处理方式:const:编译时确定其zhi;define:预处理时进行替换

2、类型检查:const:有数据类型,编译时进行数据检查;define:无类型,也不做类型检查

3、内存空间:const:在静态存储区储存,仅此一份;define:在代码段区,每做一次替换就会进行一次拷贝

4、define可以用来防止重复定义,const不行

三、指针与引用的区别?

1、指针:一个变量,存储的内容为一个地址;引用:给一个已有对象起的别名

2、指针是一个实体,需要分配内存空间;引用知识变量别名,不需要分配内存空间

3、可以有多级指针,不能有多级引用

4、自增运算结果不一样

5、指针是间接访问,引用是直接访问

6、指针可以不用初始化,引用一定要先初始化

四、指针与数据的区别?

1、含以上的区别:数组对应着一块内存,而指针是指向一块内存。数组的地址和空间大小在生命周期不会发生改变,内容可能会发生改变,而指针指向的内存大小可以随时发生改变。当指针指向常量字符串时,它的内容不可以改。

2、计算容量的区别:用sizeof计算出数组的元素个数,无法计算指针所指向内存的大小

3、数组名是常量指针,指针是变量指针

4、对数组用&和对指针&的意义不同,此时数组名不在当成指向一个元素的常量指针来使用

五、不用临时变量实现两个变量的交换

#include
using namespace std;


void Switch(int *p1,int *p2)
{
	*p1 = *p1 + *p2;
	*p2 = *p1 - *p2;
	*p1 = *p1 - *p2;
}

void Xor(int *p1, int *p2)
{
	*p1 = *p1^*p2; //异或操作
	*p2 = *p1^*p2;
	*p1 = *p1^*p2;
}

int main()
{
	int a = 1, b = 2;
	int *p1 = &a;
	int *p2 = &b;
	cout << "*p1 = " << *p1 << endl;
	cout << "*p2 = " << *p2 << endl;

	Switch(p1, p2);
	cout << "*p1 = " << *p1 << endl;
	cout << "*p2 = " << *p2 << endl;

	Xor(p1,p2);
	cout << "*p1 = " << *p1 << endl;
	cout << "*p2 = " << *p2 << endl;

	system("pause");
	return 0;
}

方法一缺陷:相加和可能存在溢出情况。

六、函数指针与指针函数

函数指针:顾名思义,与整型指针类似,整型指针为指向整型的指针,函数指针为指向函数的指针,是指针变量,他与函数名无挂,只与参数列表和返回类型有关;

指针函数:本质是函数,返回值为一个指针。

#include
using namespace std;

int ADD(int a, int b)//求和函数
{
	return a + b;
}

int Sub(int a, int b)//做差函数
{
	return a - b;
}

int* add(int *p1, int *p2)//求和函数
{
	int a = *p1 + *p2;
	int *p = &a;
	return p;
}

int main()
{
	int a = 5, b = 3;
	int *p1 = &a;
	int *p2 = &b;
    
	//声明一个函数指针,只能指向两个整型参数,返回值为Int型的函数
	int (*hanshuzhizhen)(int,int);

	hanshuzhizhen = ADD;  //函数指针初始化,使函数指针指向ADD函数;
	cout <<"函数指针指向ADD函数计算结果:" <

七、一个C++源文件从文本到可执行文件经历的过程

1、预处理:对所有的define进行宏替换;处理所有的条件编译#idef等;处理#include指令;删除注释等;bao#pragma

2、编译:将预处理后的文件进行词法分析、语法分析、语义分析以及优化相应的汇编文件

3、优化:

4、汇编:将汇编文件转换成机器能执行的代码

5、链接:包括地址和空间分配,符号决议和重定位

八、C++11新特性

1、nullptr代替NULL,传统C++在识别NULL有两种情况,一种是空指针,一种是当做0,在重载时往往把应该看成指针的当做0处理

2、类型推导:auto 和decltype(可以让编译器找出表达式的类型)

3、区间迭代,使c++的for语句能向python一样便捷

4、初始化列表

5、模板增强

6、新增容器 :std::array,std::forward_list(单链表)

7、正则表达式

8、线程支持

9、右值引用(重点)

九、C++和C的不同

1、c语言是面向过程的程序设计,主要核心为:数据结构和算法,具有高效的特性。对于C语言程序的设计,主要是考虑如何通过一个过程,对输入进行处理得出一个输出。C++是面向对象的程序设计,对于C++,首先考虑的是如何构造一个对象模型,让这个模型配合对应问题,这样可以通过获取对象状态信息得到输出

2、C++比C语言的增强点:1、命名空间2、实用性加强3、register关键字4、变量检测加强5、struct 加强

十、malloc的原理

函数原型: void* malloc(size_t  n)返回值额类型为void*,为动态分配得到的内存,但代大小是确定的,不允许越界使用。

malloc函数的实质体现在它有一个可以将可用内存块连接成一个长的列表的空闲链表,当调用链表时,它沿着连接表寻找一个大到可以满足用户请求所需内存,将内存一分为二,将分配给用户那块内存传给用户,剩下的那块返回连接表。

十一、内存泄漏、野指针

内存泄漏:动态申请的内存空间没有被正常释放,但也不能继续被使用的情况

野指针:指向被释放的内存或者访问受限的指针   造成的原因:1、指针未被初始化 2、被释放的指针没有被置为NULL 3、指针越界操作

解决内存泄漏的办法:使用智能指针

十二、static

1、局部静态变量:static局部变量和普通局部变量有什么区别?

static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;程序的局部变量存在于(堆栈)中,全局变量存在于(静态区 )中,动态申请数据存在于( 堆)中。

2、全局静态变量:static全局变量与普通的全局变量有什么区别?

答 、全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

3、静态成员函数:static函数与普通函数有什么区别?

static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件,static函数与普通函数有什么区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

十三、union和struct

1、在存储信息时,struct可以存储多个成员,而union每个成员会共享一个存储空间,只能存储最后一个成员

2、在任何时刻,union只存放被选中的那个成员,struct每个成员都在

3、对union的不同成员赋值,将会对其他成员重写

十四、new与malloc的区别

1、属性:new为关键字,malloc为库函数,需要头文件支持

2、参数:使用new申请内存无需指定内存大小,编译器会自行计算,而malloc需要显示的给出所需内存的大小

3、返回类型:new分配成功返回的是对象类型指针,与对象严格匹配,无需类型转换,故new是符合类型安全性操作符,malloc返回的是void*

4、分配失败:new分配失败,抛出bad_alloc异常,malloc则是返回NULL

5、重载

6、内存区域:new分配的内存在自由储存区,malloc在堆上分配内存

十五、C++类型转换

1、static_cast

2、dynamic_cast

3、const_cats

4、reinterpret_cast

#include
//using namespace std;

//static_cast()其中typeid可以为一般类型,也可以为指针引用

class A
{
public:
	A() :i(1), j(1) {}
	~A() {}

	void printA()
	{
		std::cout << "call printA() in class A" << std::endl;
	}

	void printSum()
	{
		std::cout << "Sum = " << i + j << std::endl;
	}

private:
	int  i, j;
};


class B :public A
{
public:
	B():a(2),b(3){}
	~B(){}

	void printB()
	{
		std::cout << "call printB() in class B" << std::endl;
	}

	void printSum()
	{
		std::cout << "Sum = " << a + b << std::endl;
	}

	void Add()
	{
		a++;
		b++;
	}

private:
	double a, b;
};


int main1()
{
	B *ptrB = new B;          //创建一个B类型对象。堆区
	ptrB->printSum();         //输出和

	A *ptrA = static_cast(ptrB);  //将派生类转化成父类,上行转换
	ptrA->printA();           
	ptrA->printSum();         

	ptrA = new A;                 //创建一个父类对象
	ptrB = static_cast(ptrA); //将父类对象转换成子类对象,下行转换
	ptrB->printB();  
	ptrB->printSum(); 


	B b;                         //栈上创建一个B类型对象
	B &rB = b;                   //对b的引用
	rB.printSum();               
	A &rA = static_cast(b); //派生类转换成基类,上行转换
	rA.printA();  
	rA.printSum();

	A a;  
	A &rA1 = a; 
	rA1.printA(); 
	B &rB1 = static_cast(a); //将基类转换成派生类,下行转换
	rB1.printB(); 
	rB1.printSum(); 

	system("pause");
	return 0;
}

十六、面向对象的了解

面向对象:把数据和对数据的操作方法放在一起,做成一个相互依靠的整体,称之为对象,对同类对象抽象出共同特性,类中大多数数据只能用本类的方法进行处理

面向对象的三大特性:封装,继承,多态

封装:将一类事物的属性和行为抽象为一个类,使属性私有化,行为公开化,提高数据隐蔽性,复用性高

继承:进一步将属性和行为抽象为一个父类,而每一个子类拥有父类的行为和属性,也有自己的行为和属性

多态:接口复用

十七、前置和后置的区别

前置++的实现比较高效,自增之后,将*this指针直接返回即可,一定要返回this指针

后置++的实现比较麻烦,因为要返回自增之前的对象,所以先将对象进行拷贝一份,再进行自增,最后返回那个拷贝

十八、静态库和动态库

静态库:1、链接时将程序放进可执行的程序中

              2、可产生多个副本

              3、不依赖程序运行

动态库:1、程序运行时,加载时才会到动态库找函数

               2、多线程共享

               3、依赖程序运行

十九、struct内存大小的确定

存在内存对齐的缘故,对于32位机器,是4字节对齐,64位机器是8字节对齐。

struct A
{
    int  a;
    char b;
    int  c;
}

如上代码:4 + 4 + 4 = 12字节

二十、strlen,strcpy函数的实现

#include
#include"test.h"
using namespace std;
//将src字符串复制到desc中
char* mystrcpy(char* desc, const char* src)
{
    if (desc == NULL || src == NULL) //内存正常判断
    {
        return NULL;
    }
    char* str = desc;
    while ( (*str = *src) != '\0')//‘\0'字符串结束标志
    {
        str++;
        src++;
    }
    return desc;
}


//将src字符串连接到desc中
char* mystrcat(char* desc, const char* src)
{
    if (desc == NULL || src == NULL)
    {
        return NULL;
    }
    char* str = desc;
    while (*src++ != '\0'); // 第一步:找出目的字符串的结尾
    src--;
    while ((*desc++ = *src++) != '\0');//第二步:将源字符串添加到目的字符串
    return str;
}


//将src字符串前n个字符复制到desc中
char* mystrncpy1(char* desc, const char* src, int n)
{
    char* str = desc;
    //前面应该有个目的内存是否不足判断
    int m = 0;
    while (*str != '\0')
    {
        str++;
        m++;
    }


    if (desc == NULL || src == NULL || n>m ) //内存正常判断
    {
        return NULL;
    }


    while (n--)
    {
        *str++ = *src++;
    }
    return desc;
}


//比较两个字符串大小
int mystrcmp(const char* str1, const char* str2)
{
    while (*str1 && *str2 && *str1 == *str2)
    {
        str1++;
        str2++;
    }
    return *str1 - *str2;
}


//求出字符串str长度
int mystrlength(const char* src)
{
    if (src == NULL)
    {
        return 0;
    }


    int n = 0;
    while (*src++ != '\0')
    {
        n++;
    }
    return n;
}

二十一、memcpy,memset内部函数实现

#include
using namespace std;
/*
memcpy函数用于资源内存(src指向的内存)拷贝到目标内存(desc指向的内存);拷贝的个数size
用法::(1)可以拷贝任何类型的对象,因为函数的参数类型是void* ,由于函数拷贝是一个字节一个
      字节拷贝,实际操作是将void*强制转换成了char*,这样才能保证每一次加一个指针
*/


//实现,未考虑内存重叠情况
void* memcpy1(void * desc, const void * src, size_t size)
{
    if (desc == NULL && src == NULL)
    {
        return NULL;
    }
    unsigned char* desc1 = (unsigned char*)desc; //将void*转成unsigned char*类型
    unsigned char* src1 = (unsigned char*)src;   //将void*转成unsigned char*类型


    while (size-->0)
    {
        *desc1++ = *src1++;
    }
    return desc;  
}


//考虑内存重叠的情况
void* memcpy2(void* desc, const void * src, size_t size)
{
    if(desc == NULL && src == NULL)
    {
        return NULL;
    }
    unsigned char* desc1 = (unsigned char*)desc;
    unsigned char* src1 = (unsigned char*)src;
    //当内存重叠时,从后往前复制
    if(desc > src && desc1 < (src1 + size))//内存发生重叠
    { 
        for (int i = size - 1; i >= 0; i--)
        {
            *desc1++ = *src1++;
        }
    }
    else
    {
        for (int i = 0; i < size; i++)
        {
            *desc1++ = *src1++;
        }
    }
    return desc;
}


//memset内部实现memset(void*s,int ch,size_t n)
/*将s所指向的某一块内存中的前n个字节的内容全部设置为ch指定的ASCII值,返回s*/
void * memset1(void * ptr, int value, size_t n)
{
    if (ptr == NULL && n < 0)
    {
        cout << "赋值出现错误" << endl;
        return;
    }
    char *s = (char*)ptr; //定义一个指针来接str;辅助指针
    while (n--)
    {
        *s++ = (char)value;
    }
    return ptr;
}



下面就是根据源码而来的memset实现:

void* memset(void* dst,int val, size_t count)
{
    void* ret = dst;
    while(count--)
    {
        *(char*)dst = (char)val;
        dst = (char*)dst + 1; 
    }
    return ret;
}

二十二、C/C++内存管理方式,内存分配

内存分配方式:在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区。

栈:在执行程序过程中,局部作用域出现的一些局部变量可以在栈上创建,等脱离该作用域创建的内存被释放。栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

堆:用于程序内存动态分配,用c/c++中的new/malloc分配,delete/free释放。堆则是 C/C++ 函数库提供的,它的机制是很复杂的。

自由存储区:它是C++基于new操作符的一个概念,凡是通过new操作符申请的内存即为自由存储区

全局/静态存储区:这块内存在程序编译期间已经分配好,在程序整个运行阶段一直存在。全局变量个和静态变量

常量存储区:特殊的一块内存,里面存放的是常量,不允许修改。

二十三、深拷贝和浅拷贝

浅拷贝:只是拷贝了基本类型的数据,而引类型数据,复制后也是会发生引用,浅拷贝只是指向被复制的内存地址,如果原来对象呗修改,那么浅拷贝出来的对象也会被修改。

深拷贝:在计算机中开辟一块新内存用于存放复制的对象。因此要用new或者malloc等。

二十四、debug和release的区别

debug:通常称为调试版本,包含着调试信息,便于程序员调试

release:称为发布版本,它往往是经过各种优化,使得程序在代码大小和运行速度是最优的,方便用户使用。

二十五、main是否需要返回值?

       main 函数的返回值用于说明程序的退出状态。如果返回 0,则代表程序正常退出,否则代表程序异常退出。有些编译器在main函数中没有添加return语句,但还是能正常编译通过是因为编译器要自动在生成的目标文件中添加return 0,但还是建议加上,提高程序的移植性。

二十六、C++动态链接库与C动态链接库

C调用C++动态链接库,需要把C++动态链接库用C++动态库提供的类再进行API封装

C++调用C动态链接库,需要加上extern C,用C编译准则来编译

二十七、结构体

结构体比较:C++:重载==,但是要注意内存对齐,为了提高CPU的读取内存的效率更高。改变内存对齐方法:宏定义

C语言中不能比较,内存对齐,会有垃圾填充,造成干扰。

二十八、拷贝构造函数为什么传引用?

原因:参数为引用,不为值传递是为了防止拷贝构造函数的无限递归,最终导致栈溢出。这也是编译器的强制要求。

#include

class Example
{
private:
	int m_test;
public:

	//带参数的构造函数
	Example(int x) :m_test(x)
	{
		std::cout << "constructor with argument!" << std::endl;
	}

	//拷贝构造函数
	Example(const Example &test)
	{
		m_test = test.m_test;
		std::cout << "copy constructor" << std::endl;
	}

	//赋值运算符重载
	Example&  operator=(const Example &test)
	{
		std::cout << "assignment operator" << std::endl;
		m_test = test.m_test;
		return *this;
	}

	void Print(Example test)
	{

	}
};

int main()
{
	Example aaa(2);
	Example bbb(3);

	bbb = aaa;

	Example ccc = aaa;
	Example ddd(aaa);
	bbb.Print(aaa);

	system("pause");
	return 0;
}
constructor with argument!  // Example aaa(2);
constructor with argument!  // Example bbb(3);
assignment  operator        // bbb = aaa;
copy constructor            // Example ccc = aaa;
copy constructor            // Example ddd(aaa);
copy constructor            // bbb.Print(aaa);

第一二就不解释了

至于第三个,bbb是已经实例化的对象,不需要构造,因此只会调用=运算符重载函数,第四个的ccc还没有实例化,因此要调用拷贝构造函数,构造出ccc,而不是=运算符重载函数

第五个就是拷贝构造函数的应用,

第六个:实际是将aaa作为参数传递给bbb.Print(Example  ex),即Example  ex = aaa,和第四个一致,所以还是调用拷贝构造函数。

通过这个例子, 我们来分析一下为什么拷贝构造函数的参数只能使用引用类型。

看第四个输出: copy constructor                      //CExample ccc = aaa;

构造ccc,实质上是ccc.CExample(aaa); 我们假如拷贝构造函数参数不是引用类型的话, 那么将使得 ccc.CExample(aaa)变成aaa传值给ccc.CExample(CExample ex),即CExample ex = aaa,因为 ex 没有被初始化, 所以 CExample ex = aaa 继续调用拷贝构造函数,接下来的是构造ex,也就是 ex.CExample(aaa),必然又会有aaa传给CExample(CExample ex), 即 CExample ex = aaa;那么又会触发拷贝构造函数,就这下永远的递归下去。

二十九、程序崩溃原因

1、读取未赋值的变量

2、函数栈溢出

3、数组越界访问

4、指针的目标对象不可用

三十、C++字符串输入

1、cin>>

用法1:最基本,也是最常用的用法,输入一个数字:

用法2:接受一个字符串,遇“空格”、“TAB”、“回车”都结束

#include
using namespace std;
main ()
{
char a[20];
cin>>a;
cout< }

输入:jkljkljkl
输出:jkljkljkl

输入:jkljkl jkljkl //遇空格结束,所以不能输入多个单词
输出:jkljkl

2、cin.get()

用法1: cin.get(字符变量名)可以用来接收字符

#include
using namespace std;
main ()
{
char ch;
ch=cin.get(); //或者cin.get(ch);只能获取一个字符
cout< }

输入:jljkljkl
输出:j

用法2:cin.get(字符数组名,接收字符数目)用来接收一行字符串,可以接收空格

#include
using namespace std;
main ()
{
char a[20];
cin.get(a,20); //有些类似getline。可以输入多个单词,中间空格隔开。
cout< }

输入:jkl jkl jkl
输出:jkl jkl jkl

输入:abcdeabcdeabcdeabcdeabcde (输入25个字符)
输出:abcdeabcdeabcdeabcd (接收19个字符+1个'\0')

用法3:cin.get(无参数)没有参数主要是用于舍弃输入流中的不需要的字符,或者舍弃回车,弥补cin.get(字符数组名,接收字符数目)的不足.

这个我还不知道怎么用,知道的前辈请赐教;

3、cin.getline()

// 接受一个字符串,可以接收空格并输出

#include
using namespace std;
main ()
{
char m[20];
cin.getline(m,5); //与上面基本相同。
cout< }

输入:jkljkljkl
输出:jklj

接受5个字符到m中,其中最后一个为'\0',所以只看到4个字符输出;

如果把5改成20:
输入:jkljkljkl
输出:jkljkljkl

输入:jklf fjlsjf fjsdklf
输出:jklf fjlsjf fjsdklf

4、getline()

接受一个字符串,可以接收空格并输出,需包含“#include

#include
#include
using namespace std;
main ()
{
string str;
getline(cin,str);
cout< }

输入:jkljkljkl //VC6中有个bug,需要输入两次回车。
输出:jkljkljkl

输入:jkl jfksldfj jklsjfl
输出:jkl jfksldfj jklsjfl

和cin.getline()类似,但是cin.getline()属于istream流,而getline()属于string流,是不一样的两个函数

5、gets()

接受一个字符串,可以接收空格并输出,需包含“#include

#include
#include
using namespace std;
main ()
{
char m[20];
gets(m); //不能写成m=gets();
cout< }

输入:jkljkljkl
输出:jkljkljkl

输入:jkl jkl jkl
输出:jkl jkl jkl

6、getchar()

接受一个字符,需包含“#include

#include
using namespace std;
main ()
{
char ch;
ch=getchar(); //不能写成getchar(ch);
cout< }

输入:jkljkljkl
输出:j

//getchar()是C语言的函数,C++也可以兼容,但是尽量不用或少用;

三十一、lambda表达式

       一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数相似,一个lambda具有一个返回类型、一个参数列表、一个函数体。但与函数不同,lambda可以定义在函数内部,一个lambda表达式具有如下形式:

[capture list捕获列表](parameter list参数列表) {函数体},例子如下:

auto  f = [](const string &a,const string &b){return a.size() < b.size()}

        我们可以忽略参数列表和返回类型,但永远包含捕获列表和函数体,其中捕获列表内容通常为空。

三十二、C++编程优化

1. 对齐原则。比如64位总线,每次寻址读取8B。编程时注意变量地址,尽量消耗总线最少的寻址次数。堆内存申请时,系统严格按照对齐原则分配,故而使用时候也尽量不要跨寻址边界。

2. 需要的时候,可为了效率拷贝代码,虽然增加了代码体积,但这是值得的。尤其是for循环,若次数比较少,拆开亦无妨。

3. 位运算中,-1右移,左边补1,故仍为-1;-1左移,右边补0,故不再为-1。

4. 每次申请的堆内存,最好初始化,里面是垃圾数据,而并非为空。

5. 项目开发中,往往一个引擎对外暴露的是一个纯虚类,而其接口就是这个类的**指针变量。

6. 程序逻辑,重在语义。不能为代码的过分简单而减少函数的设计。

7. *&表示对指针的引用。

8. 类的静态方法不可调用其非静态方法,亦不可调用非静态成员变量。

9. 多文件编程时,头文件不可相互包含。

10. 头文件里尽量不要使用using namespace std;

11. static成员定义要放在cpp文件里面,而不是头文件里。

12. 纯虚类尽量不要延续两层以上。

13. include引用尽量都放在cpp文件里。

14. 子类继承父类,不可在构造函数里初始化父类并未在构造函数里初始化的成员,也就是说,子类构造函数里能初始化的成员,只有自己本身的和父类构造函数里的。

15. 项目开发中,对于一些依赖本地环境的参数,要写专门的配置文件,比如服务器地址。

16. 头文件里只声明,不定义。在头文件中,全局变量声明,必须加extern修饰;静态成员变量声明放在头文件,定义放在cpp文件,若是普通静态变量,最好声明和定义放在cpp,因为static作用域限于单文件,放在cpp里只对本文件可见,放在头文件会被所有引用该头文件的cpp拷贝一份完全相同的变量。

17. linux下,进行文件操作时,文件路径要采用绝对路径(相对路径很多时候会出bug),文件指针要对其返回值作判断,防止空指针。

18. debug状态下使用assert是极好的,不过记得发布版本前在#include 前加上#define NDEBUG,assert语句会被NDEBUG所影响。这里多嘴一句,错误与异常是不同的,异常是不可避免,在发布版本里不可或缺的,故而assert不能用于处理异常。注:在加上#define NDEBUG后,不论是调试还是运行,assert语句都会被直接忽略,故而平时开发时把#define NDEBUG注释掉,发布时再启用它。

19. 面向对象编程:可维护,可复用,可扩展,灵活性好。

20. 频繁使用的代码里,尽量少使用容器,因为其创建和回收的开销很大。

21. 字符串拼接效率:经过测试(http://bbs.csdn.net/topics/360000434和我自己项目中的实验),memcpy效率最高,string类的+=效率特别高,足以满足日常所需,而strcat效率很差,append也不快。

22. 基类中的虚函数要么实现,要么是纯虚函数(绝对不允许声明不实现,也不纯虚)。

23. 在C++的类中,普通成员函数不能作为pthread_create的线程函数,如果要作为pthread_create中的线程函数,必须是static。

24. 当多个线程访问同一个类静态方法时,若该方法里只操作栈上数据,则无妨,因为线程的栈是独立的;若该方法操作了非栈上的数据(比如堆、全局变量等),则需要互斥锁。

25. 内联函数,定义体要放在头文件,以保证每一个引用该头文件的cpp里都是完全相同的拷贝;inline关键字置于定义前,不必放在声明前;若定义在类内部,可不需要inline关键字。

26. vector执行clear时,会自动调用元素(如果是类的话)的析构函数。

27. 编程时,对变量(尤其是全局性质的变量或类)命名,要用解释性的,而不能用随意的j1,i1,n,m等名称,容易与库里的变量冲突。

28. 定义宏时,尽量多用整数,少用-1,-2之类,容易受uint和int不统一带来的困扰。

29. 函数形参采用默认值时,要写在声明里,而不写定义里,这样方便调用其头文件的cpp看得到默认值。

31. utf-8 中文编码 三个字节表示一个汉字

32. 项目开发时,使用条件编译:

复制代码

#define DEBUG
main()
{
#ifdef DEBUG
    printf("Debugging\n");
#else
    printf("Not debugging\n");
#endif
    printf("Running\n");
}

复制代码

发布版本时,注释掉第一行。这种方式要比开大量注释来得方便。

33. 关于c字符数组,需要注意一个初始化问题:

(1) char str[10]="";
(2) char str[10]={'\0'};
(3) char str[10]; str[0]='\0';

前两者的意义在于str的每个元素都初始化为'\0',第三者仅仅初始化第一个元素。当数组长度很大时,前两者时间开销极大。故而,有些不必要的时候,不要如此初始化。

34. 判断一个字符是否为数字:

头文件:#include
定义函数:int isdigit(int c);
函数说明:检查参数 c 是否为阿拉伯数字0 到9。
返回值:若参数c 为阿拉伯数字,则返回true,否则返回null(0)。
附加说明:此为宏定义,非真正函数。

复制代码

#include 
main(){
    char str[] = "123@#FDsP[e?";
    int i;
    for(i = 0; str[i] != 0; i++)
        if(isdigit(str[i]))
            printf("%c is an digit character\n", str[i]);
}

复制代码

35. 在类里声明静态常量,尤其是数组定义的时候,如static const *str = "sdfsssf", 要在const前加上constexpr修饰,可以在编译器就将“sdfsssf”赋值给str,达到类似宏的效果。

36. std::string::find_last_of("/\\") 这里的"/\\"指的是'/'或'\',在搜索字符时用得到。

37. 项目里c字符串传递,多采用首地址+长度的方式,避免0x0存在导致的异常; 线程数要合适,大致为cpu总核数两倍以内为佳,线程间切换会一定程度上消耗程序性能。

  有一个陷阱是在c字符串转string类型时,c字符串里如有0,转化时用strlen取长度就会出错。故而,c串表示尽量维护一个len来记录长度,而不是通过结尾0来判别。另外,strlen效率低且不安全,少用。

38. 静态函数的实现写在h文件里;尽量把h文件里的函数实现前都加上inline,不论其复杂度,避免被多文件引用而引起重复定义错误。

39. 静态成员变量必须在类外进行初始化。如果是复杂类型,比如容器,也要在类外定义,如std::vector hj::sm_tr;

40. 编程时,一般结构体里的堆内存由内存池管理申请或释放,或者stl里使用这些结构体作为元素时,使用其指针,而不是实例。因为stl里内存申请或释放,会调用其元素的构造和析构,这里会有陷阱。

41. 使用set、map时,有一个陷阱,就是[]操作符。当使用这个操作符时,如果[x]里的x并不在set或map里,则stl会自动生成一个x元素,这样使得stl容器内容改变,如果开发者忽略了这点,后果可能无法预期。

你可能感兴趣的:(C++面试)