C++校招面经【基础语法】——C/C++部分

21.C++与Java的区别?

语言特性:

        Java语言给开发人员提供更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性很强。

        Java语言中没有指针的概念,引入了真正的数组。

        C++也可以在其他系统运行,但是需要不同的编码。

        Java用接口(interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省去了在实现和维护上的复杂性。

垃圾回收:

        C++用户析构函数后回收垃圾,Java中内存的分配和回收都是自动进行的。

应用场景:

        Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是C++的优势,可以直接对内存的操作,但同时具有危险性。

        Java在web应用上具有C++不可比拟的优势,具有丰富多样的框架。

        对底层程序的编程以及控制方面的编程,C++很灵活。

22.C++中struct和class的区别

相同点:

        两者都拥有成员函数、公有和私有部分。

        任何可以使用class完成的工作,同样可以使用struct完成。

不同点:

        两者中如果不对成员指定公私有,struct默认是公有的,class默认是私有的。

        class默认是private继承,struct默认是public继承。

引申:C++和C的struct区别

C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,C++中的struct能继承,能实现多态。

C中struct是没有权限的设置的,而struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。

C++中,struct增加了访问权限,而且可以和类一样有成员函数,成员默认访问说明符为public。

struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名;C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当做类的一种特例。

23.define宏定义和const的区别

编译阶段:

        define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。

安全性:

        define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错。

        const常量有数据类型,编译器可以对其进行类型安全检查

内存占用:

        define只是将宏名称进行替换,咋内存中会产生多份相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表。

        宏替换发生在编译阶段之前,属于文本插入替换;const作用发生在于编译过程中。

        宏不检查类型,const检查数据类型。

        宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

24.C++中const和static的作用

static

不考虑类的情况:

        隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用。

        默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区。

        静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。

考虑类的情况:

        static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外初始化,初始化时不需要标示为static;可以被非static 成员函数任意访问。

        static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问。

const

不考虑类的情况:

        const常量在定义时必须初始化,之后无法更改。

        const形参可以接受const和非const类型的实参。

考虑类的情况:

        const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。

        const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable数据的值。

补充:const修饰变量时也与static有一样的隐藏功能。只能在该文件中使用,其他文件不可以引用声明使用。因此在头文件中声明const变量是没有问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。

25.C++的顶层const和底层const

概念区分:

        顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是*的右边。

        底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是*号的左边。比如:

int a = 10;int* const b1 = &a;        //顶层const,b1本身是一个常量
const int* b2 = &a;       //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; 		   //顶层const,b3是常量不可变
const int* const b4 = &a;  //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a;		   //用于声明引用变量,都是底层const

区分作用:

        执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const。

        使用命名的强制类型转换const_cast时,只能改变运算对象的底层const。

        int const a 和const int a 均表示定义常量类型a。

        const int *a ,其中a为指向int型变量的指针,const在*左侧,表示a指向不可变常量。

        int *const a,依旧是指针类型,表示a为指向整型数据的常指针。

26.数组名和指针(这里为指向数组首元素的指针)区别?

        二者均可以通过增减偏移量来访问数组中的元素。

        数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

        当数组名当作形参传递给调用函数后,就失去了原有的特性,退化成了一般指针,多了自增自减操作,但sizeof运算符不能在得到原数组的大小了。

27.final和override关键字

override:

        当在父类中使用了虚函数的时候,可能需要在某个子类中对这个函数进行重写,以下方法均可:

class A
{
    virtual void foo();
}
class B: public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

如果不使用override,当输入错误时,比如将 void foo()写成了 void f00(),编译器并不会报错,因为编译器不知道你的目的是重写虚函数,而是把它当作一个新的函数。因此对整个程序就会产生恶劣的影响,所以,override的作用就出来了,他指定了子类的这个虚函数是重写的父类的,如果名字不小心打错了,编译器是不能通过的:

class A
{
    virtual void foo();
};
class B: public A
{
    virtual void f00(); //OK,这个函数是B新增的,不是继承的
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};

final

        当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或者重写,编译器会报错,例子如下:

class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

28.拷贝初始化和直接初始化

当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用月实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。

string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。

        当拷贝构造函数为private时:语句3和语句4在编译时会报错。

        使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译会报错。

29.初始化和赋值的区别

        对简单类型来说,初始化和赋值没有什么区别。

        对于复杂类型来说,这两的区别很大:

class A{
public:
    int num1;
    int num2;
public:
    A(int a=0, int b=0):num1(a),num2(b){};
    A(const A& a){};
    //重载 = 号操作符函数
    A& operator=(const A& a){
        num1 = a.num1 + 1;
        num2 = a.num2 + 1;
        return *this;
    };
};
int main(){
    A a(1,1);
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数
    A b;
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
    return 0;
}

30.extern“C”的用法

        为了能够在C++代码中调用C语言的代码:在程序中加入extern“C”后,相当于告诉编译器这部分代码是C语言写的,因此要按照C元进行编译,而不是C++,以下情况使用该方法:

        1.C++代码中调用C语言代码

        2.在C++头文件中使用

        3.多人协同开发时,可能有人擅长C,有人擅长C++

31.野指针和悬空指针

        都是指向无效内存区域(这里指的无效是指“不安全不可控”)的指针,访问行为会导致未定义行为。

野指针:

        指的是没被初始化的指针。

int main(void) { 
    
    int* p;     // 未初始化
    std::cout<< *p << std::endl; // 未初始化就被使用
    
    return 0;
}

为了防止出错,对于指针初始化时都是赋值为nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问。

悬空指针:

        指针最初指向的内存已经被释放了的一种指针。

int main(void) { 
  int * p = nullptr;
  int* p2 = new int;
  
  p = p2;

  delete p2;
}

此时,p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预测。需要设置p=p2=nullptr。此时再使用,编译器会直接报错。避免野指针比较简单,但是悬空指针比较麻烦。C++引入了只能指针,C++智能指针的本质就是避免悬空指针的产生。

产生的原因及解决办法:

        野指针:指针变量未及时初始化——定义指针变量及时初始化,要么置空。

        悬空指针:指针free或delete之后没有及时置空——释放操作后立即置空。

32.C和C++的类型安全

什么是类型安全?

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没有授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。

1.C的类型安全

        C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另外一种结构体的指针时,编译器将会报错,除非使用显示类型转换。

2.C++的类型安全

        如果C++使用得当,将比C更有类型安全,C++中提供了一些新的机制保障类型安全:

       1. 操作符new返回的指针类型严格与对象匹配,而不是void*。

        2.C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;

        3.引入const关键字代替#define constants,它是有类型。有作用域的,而#define constants只是简单的文本替换。

        4.一些#define宏可以被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全。

        5.C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。

33.C++中的重载、重写(覆盖)和隐藏的区别

1.重载

        指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否为虚函数无关。

class A{
    ...
    virtual int fun();
    void fun(int);
    void fun(double, double);
    static int fun(char);
    ...
}

2.重写(覆盖)(override)       

        在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:与基类的虚函数有相同的参数个数、参数类型以及返回值类型。

//父类
class A{
public:
    virtual int fun(int a){}
}
//子类
class B : public A{
public:
    //重写,一般加override可以确保是重写父类的函数
    virtual int fun(int a) override{}
}

重载和重写的区别:

        重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系;

        重写要求参数列表相同,重载则要求参数类标不同,返回值不要求;

        重写关系中,调用方法根据对象类型决定,重载根据调用实参列表和形参表的对应关系来选择函数体。

3.隐藏(hide)

        在某些情况下,派生类的函数屏蔽了基类中的同名函数,包括以下情况:

        两个函数参数相同,但是基类函数不是虚函数。隐藏和重写的区别在于基类函数是否是虚函数。

//父类
class A{
public:
    void fun(int a){
		cout << "A中的fun函数" << endl;
	}
};
//子类
class B : public A{
public:
    //隐藏父类的fun函数
    void fun(int a){
		cout << "B中的fun函数" << endl;
	}
};
int main(){
    B b;
    b.fun(2); //调用的是B中的fun函数
    b.A::fun(2); //调用A中fun函数
    return 0;
}

两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。

34.C++有哪几种构造函数

默认构造函数;

初始化构造函数;

拷贝构造函数;

移动构造函数;

委托构造函数;

转换构造函数;

#include <iostream>
using namespace std;

class Student{
public:
    Student(){//默认构造函数,没有参数
        this->age = 20;
        this->num = 1000;
    };  
    Student(int a, int n):age(a), num(n){}; //初始化构造函数,有参数和参数列表
    Student(const Student& s){//拷贝构造函数,这里与编译器生成的一致
        this->age = s.age;
        this->num = s.num;
    }; 
    Student(int r){   //转换构造函数,形参是其他类型变量,且只有一个形参
        this->age = r;
		this->num = 1002;
    };
    ~Student(){}
public:
    int age;
    int num;
};

int main(){
    Student s1;
    Student s2(18,1001);
    int a = 10;
    Student s3(a);
    Student s4(s3);
    
    printf("s1 age:%d, num:%d\n", s1.age, s1.num);
    printf("s2 age:%d, num:%d\n", s2.age, s2.num);
    printf("s3 age:%d, num:%d\n", s3.age, s3.num);
    printf("s2 age:%d, num:%d\n", s4.age, s4.num);
    return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s2 age:10, num:1002

        默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作。

        复制构造函数用于复制本类的对象。

        转换构造函数用于将其他类型的变量,隐式转换为本类对象。

35.深拷贝和浅拷贝的区别

浅拷贝:

        浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针资源就会出现错误。

深拷贝:

        深拷贝不仅拷贝值,还开辟出一块新的空间来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

#include <iostream>  
#include <string.h>
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student(){
        name = new char(20);
		cout << "Student" << endl;
    };
	~Student(){
        cout << "~Student " << &name << endl;
        delete name;
        name = NULL;
    };
	Student(const Student &s){//拷贝构造函数
        //浅拷贝,当对象的name和传入对象的name指向相同的地址
        name = s.name;
        //深拷贝
        //name = new char(20);
        //memcpy(name, s.name, strlen(s.name));
        cout << "copy Student" << endl;
    };
};
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}
//浅拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***

//深拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0

从执行结果看,浅拷贝在对象创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,格字成员地址不同。

36.内联函数和宏定义的区别

        在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。

        内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。

        宏定义时要注意书写(参数要括起来)否则容易出现歧义,内敛函数不会产生歧义。

        内敛函数有类型检测、语法判断等功能,宏没有。

内敛函数使用场景:

        使用宏定义的地方都可以使用inline函数。

        作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。 

37.public,protected和private访问和集成权限/public/protected/private的区别?

访问权限:

访问权限外部     派生类内部
        public        √         √        √
        protected        ×        √        √
        private        ×        ×        √

 继承权限:

        1.派生类继承自基类的成员权限有四种状态:public、protected、private和不可见。

        2.派生类对基类成员的访问全新取决于两点:①继承方式②基类成员在基类中的访问权限。

        3.派生类对基类成员的访问权限是取以上两点中的更小的访问范围(除了private的继承方式遇到private成员是不可见外),如:

        public继承+private成员=private;

        private继承+protected=private;

        private继承+private=不可见;

38.如何用判断代码大小端存储?

        大端存储:数据的高字节存储在底地址中;

        小端存储:数据的低字节存储在底地址中。

        所以在socket编程中,往往需要将操作系统所用的小端存储IP地址转换为大端存储,这样才能进行网络传输。

方式一:使用强制类型转换

#include <iostream>
using namespace std;
int main()
{
    int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}

方式二:union

#include <iostream>
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
    int a;
    char ch;
};
int main()
{
    endian value;
    value.a = 0x1234;
    //a和ch共用4字节的内存空间
    if (value.ch == 0x12)
        cout << "big endian"<<endl;
    else if (value.ch == 0x34)
        cout << "little endian"<<endl;
}

 39.volatile、mutable和explicit关键字的用法

1.volatile

        一种类型修饰符,用它声明的类型变量表示可以被某些编译器位置的因素更改,比如:操作系统、硬件或者其他线程等。遇到这个关键字声明的变量,编译器对访问改变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

        当要求使用volatile声明的变量的值的时候,系统总是重新从他所在的内存读取数据,即使它前面的指令刚刚从该处读过数据。

        volatile定义变量的值是易变的,每次用到这变量的值的时候都要重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。

        多线程下的volatile:该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。

2.mutable

        为了突破const的限制而设置的。有时候我们需要再const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后面关键字位置。

class person
{
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
    void add() const//在函数里不可修改this指针指向的值 常量指针
    {
        m_A = 10;//错误  不可修改值,this已经被修饰为常量指针
        m_B = 20;//正确
    }
};

3.explicit

        用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换。

40.什么情况下会调用拷贝构造函数

        用类的一个实例化对象去初始化另一个对象的时候;

        函数的参数是类的对象时(非引用传递);

        函数的返回值是函数体内局部对象的类的对象时,此时虽然发生NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值