C/C++常见面试题

31.&&和&、||和|有什么区别

运算符:&&是逻辑与, 而&是按位与;||是逻辑或,而|是按位或

30.short i = 0; i = i + 1L;这两句有错吗

i + 1L, short和long相加, 会发生long到short的转换,需要强制转换i + (short) 1L

29.谈谈你对编程规范的理解或认识

编程规范是编写高质量、可维护和可重用代码的重要保障。遵循编程规范能够提高代码的可读性、可维护性、可复用性和可靠性,促进团队协作和提高开发效率,是每位开发者都应该重视和遵守的基本原则。

28.简述队列和栈的异同

队列:先入先出,
栈:先入后出,

27.链表和数组有什么区别?

存储:数组是一整块连接的存储区域,链表则可能是多块存储区域;
访问:数组可以通过下标访问,而链表只能通过从头节点逐次移动指针进行访问;
链表便与插入和删除,并且不会出现越界的情况。

26.简述多态

对于一个类,如果存在虚函数,则编译器会为其生成一个虚表(虚函数表),还会插入一根虚指针,指向这个虚表,虚表中每一个包含指向对应虚函数的指针。当调用构造函数时候,编译器会自动将虚表指针与虚表关联,将vptr指向vtable。

主要用法是基类的指针指向子类的对象,在调用父类子类同名虚函数的时候,会调用派生类的虚函数,引用也是同理,调用的函数取决于指针指向对象的类型。

class Base 
{
public:
    void fun1() 
    { 
        fun2(); 
    }
    
    virtual void fun2()  // 虚函数
    { 
        cout << "Base::fun2()" << endl; 
    }
};

class Derived : public Base 
{
public:
    virtual void fun2()  // 虚函数
    { 
        cout << "Derived:fun2()" << endl; 
    }
};

int main() 
{
    Derived d;
    Base * pBase = & d;
    pBase->fun1();
    return 0;
}

上述代码的输出是 “Derived:fun2()”,如不解,可以将基类中的func2()替换成this->func2();在Base::fun1()成员函数体里执行this->fun2()时,实际上指向的是派生类对象的fun2成员函数。

在非构造函数,非析构函数的成员函数中调用「虚函数」,是多态!!!

「多态」的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。

每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着虚函数表的指针。「虚函数表」中列出了该类的「虚函数」地址。

25.简述类成员函数的重写、重载和隐藏的区别

重写:显示声明为overrid,用于派生类重写基类的函数,参数列表,函数名一定相同,若有差异,编译会报错;重写后;重写的函数在基类中必须被virtual修饰。

重载:是一个类中重载函数,函数参数列表一定不同;

隐藏:隐藏和被隐藏的函数不在同一个类中,函数名相同;当参数列表不一样时,无论基类的函数是否被virtual修饰,基类的函数都会被隐藏,而不是重写。

重载:静态多态。重写:动态多态。

class Base
{
public:
    virtual void f0() { cout << "Base::f0()...." << endl; }

};
 
class Derive : public Base{
public:
    void f0(int a) {
        cout << "Derive::f0(int a)..." << endl;
    }
    
    // void f0() override {
    //     cout << "Derive::f0() override..." << endl;    
    // }
    void f0() {
        cout << "Derive::f0()   hide..." << endl;  
    }
}

24.访问基类的私有虚函数

#include <iostream>
#include <stdio.h>
using namespace std;

class person
{
public:
    virtual void name()
    {
        cout<<"A::name"<<endl;
    }
    
private:
    virtual void sex()
    {
        cout<<"A::sex"<<endl;
    }
};

class student : public person
{ 
public:
    virtual void name()
    {
        cout<<"B::name"<<endl;
    }
    
    virtual void address()
    {
        cout<<"B::address"<<endl;
    }
    
private:
    virtual void ID()
    {
        cout<<"B::ID"<<endl;
    }
};

typedef void (*Fun)(void);//定义一个函数指针

int main()
{
    student stu;
    for(long i = 0; i < 4; i++)
    {
        Fun p = (Fun)*((long*) * (long*)(&stu)+i);
        p();
    }

/**
	*(long*)(&stu) 根据对象的虚函数表指针找到对应的虚函数表,指向虚函数表的首地址
	*((long*) *(long*)(&stu)+i) 对虚函数表的函数进行偏移调用(虚函数表中的函数也是一个个的指针,指向代码区中存放函数的地方)
*/
   return 0;
}

如图:
person类的虚函数

student类的虚函数表

student派生类因为重写基类的name()函数,所以虚函数表指向重写后的自己的name()

24.用C++设计一个不能被继承的类

思考:如果直接把构造函数和析构函数设置为私有的话,虽然该类不能被继承,但同样不能实例化;所以使用友元,因为友元的关系是不能通过继承获得的,所以先让A的构造和拷贝成为私有,然后让B成为A的友元,并且让B虚继承A;此时如果C继承B的话,C是没有办法添加B的虚表中,所以无法通过B调用A的构造函数。

template<typename T>
class A {
  public:
    friend T;
  private:
    A() {};
    virtual ~A(){};
};

class B : public virtual A<B> {
  public:
    B(){std::cout << "B\n";}
};
// Class C : public B {}; 


class TaskManager final {/*..*/} ; 
class PrioritizedTaskManager: public TaskManager {
};  //compilation error: base class TaskManager is final

这里需要说明的是:我们设计的不能被继承的类B对基类A的继承必须是虚继承,这样一来C类继承B类时会去直接调用A的构造函数,而不是像普通继承那样,先调用B的构造函数再调用A的构造函数;

虚继承时子类的虚函数不再是添加到父类部分的虚表中,而在普通的继承中确实直接添加到父类的虚表中,这就意味着如果虚继承中子类父类都有各自的虚函数,在子类里面就会多出一个虚表指针,而普通的继承却不会这样。

23.谈谈堆拷贝构造函数和赋值运算符的认识

拷贝构造函数:通过一个已经存在的对象来生成一个新的对象实例;是一种构造函数,创建新的实例;拷贝构造函数必须以引用的方式传递参数。

赋值运算符:将一个已经存在的对象的值复制给另一个已经存在的实例;是一种运算;

class Person
{
public:
	Person(){}
	Person(const Person& p)
	{
		cout << "Copy Constructor" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "Assign" << endl;
		return *this;
	}

private:
	int age;
	string name;
};

void f(Person p)
{
	return;
}

Person f1()
{
	Person p;
	return p;
}

int main()
{
	Person p;
	Person p1 = p;    // 1
	Person p2;
	p2 = p;           // 2
	f(p2);            // 3

	p2 = f1();        // 4

	Person p3 = f1(); // 5

	getchar();
	return 0;
}

拷贝和复制运算符都涉及到深浅拷贝,主要是当类成员包含指针时,浅拷贝会使得两个指针指向同一块区域(指针只是简单的值拷贝),此时需要自行编写拷贝构造及复制运算符。

// 深拷贝
    Person(const Person& p) {
      this->age = new int;
      std::memcpy(this->age, p.age, sizeof(int));
    }
    int *age;

22.C++的空类有哪些成员函数?

默认(缺省)构造函数,默认(缺省)析构函数,默认(缺省)拷贝构造,默认(缺省)赋值运算符,默认(缺省)取址运算符,默认(缺省)取址运算符const

编译器只有在你使用到对应的函数时候,才会去定义。

21.面向对象的三大特征

封装:明确标识出允许外部使用的所有成员函数和数据;将客观事物抽象成类,每个类可以构建自己的数据和方法;通常类的成员是私有的,一部分方法是私有的,一部分方法是共有的;封装能够使得程序更模块化,更易读写,提升了代码的重用性;

继承+多态:

继承:继承基类的方法,并做出自己的改变或扩展(解决了代码重用问题);声明某个子类兼容于基类,使得外部调用这无需关注其差别;

多态:基于对象所属类的不同,外部对同一个方法的调用,实际的执行逻辑不同;多态依附于继承。

22.设置地址为 0x67a9 的整型变量的值为 0xaa66

*(int *)0x67a9 = 0xaa66;  // store int value 10 at address 0x16 

请注意,这假定地址0x67a9 是可写的 – 在大多数情况下,这会产生异常。

通常,您只会对没有操作系统且需要写入特定内存位置(例如寄存器、I/O 端口或特殊类型的内存(例如 NVRAM))的嵌入式代码等执行此类操作。

您可以像这样定义这些特殊地址:

volatile uint8_t * const REG_1 = (uint8_t *) 0x67a9; 

然后在您的代码中,您可以像这样读取写入寄存器:

uint8_t reg1_val = *REG_1; // read value from register 1 
*REG_2 = 0xff;             // write 0xff to register 2 

关于Zeno Chen

本人涉及的领域较多,杂而不精 程序设计语言: Perl, Java, PHP, Python; 数据库系统: MySQL,Oracle; 偶尔做做电路板的开发,主攻STM32单片机
此条目发表在C/C++分类目录。将固定链接加入收藏夹。