面试之CPP基础知识-5


#知道C++中的组合吗?它与继承相比有什么优缺点吗?

一:继承

继承是Is a 的关系。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点:

  • 把父类的内部实现细节暴露给了子类, 子类的实现会和父类的实现紧密的绑定在一起, 结果是父类实现的改动,会导致子类也必须得改变。

二:组合

组合是has A的关系。也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

  • 组合的类的内部细节是不可见。
  • 相互依赖较小,低耦合。

组合的缺点:

  • 容易产生过多的对象。
  • 为了能组合多个对象,必须仔细对接口进行定义。

https://blog.csdn.net/K346K346/article/details/55045295

组合继承
has-a关系is-a关系
运行期决定编译期决定
不破坏封装,低耦合破坏,子类依赖父类
支持扩展扩展必须实现父类方法
动态选择组合类方法复用父类方法

#函数指针?

1) 什么是函数指针?

函数指针就是一个指针指向某个函数,因为在程序中如果定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,函数名表示的就是这个存储空间的首地址。

2) 函数指针的声明方法

函数返回值类型 (* 指针变量名) (函数参数列表);

3) 两种方法赋值:

指针名 = 函数名; 指针名 = &函数名

可以用于回调函数

#说一说你理解的内存对齐以及原因

  1. 分配内存的顺序是按照声明的顺序,不满对齐数则翻倍。
  2. Linux默认以4字节对齐,可通过#pragma pack(n) 修改对齐字节数。
  3. 字节对齐后可以方便系统读取

对齐规则:

  1. 基本类型的对齐值就是其sizeof值;
  2. 结构体的对齐值是其成员的最大对齐值;
  3. 编译器可以设置一个最大对齐值,类型的实际对齐值是该类型的对齐值与设置的对齐值取最小值得来。

#结构体变量比较是否相等

  1. 重载==操作符
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct foo {

  int a;
  int b;

  bool operator==(const foo& rhs) const
  {
    return( a == rhs.a) && (b == rhs.b);
  }
};
  1. 元素的话,一个个比;

  2. 指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;

#函数调用过程栈的变化,返回值和参数变量哪个先入栈?

  1. 调用方将函数的参数按照从右到左的顺序压入栈中。再将函数的返回地址压入栈中。
  2. 调用方执行函数调用指令,将控制权转移到被调用函数。
  3. 被调用函数为局部变量分配内存,并将它们按照定义顺序压入栈中。
  4. 被调用函数执行完毕后,将返回值存储在约定的位置。
  5. 被调用函数弹出栈中数据,直到弹出返回地址,然后跳回调用方的代码处。
  6. 调用方从约定处取回返回值(如果有的话)。

from GPT

#define、const、typedef、inline的使用方法?他们之间有什么区别?

宏定义typedefconstinline
宏定义,相当于字符替换定义类型别名变量不能修改调用处字符替换
预处理阶段编译阶段编译阶段编译阶段
无类型安全检查有类型安全检查
不是语句是语句加分号分配内存分配内存
有返回值

#define p_int int * 显示 int*
typedef int *p_int; 显示p_int

#你知道printf函数的实现原理是什么吗?

C/C++的函数参数是通过压入栈的方式来给函数传参数的,并且从右到左入栈。printf第一个被找到的参数就是那个字符串指针,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些算出数据需要的堆栈指针的偏移量,然后依次弹出数据进行填充。

#为什么模板类一般都是放在一个h文件中

将模板类的声明和定义放在头文件中是为了支持模板的编译和实例化,并提供更好的可读性和可维护性。

from GPT


  1. 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。

所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

  1. 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。

所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。

然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

#cout和printf有什么区别?

  1. 语法和用法:
    • cout 是 C++ 中的输出流对象,可以使用流操作符 << 来输出各种类型的数据。例如:cout << "Hello, World!"
    • printf 是 C语言中的函数,它使用格式化字符串来输出数据。例如:printf("Hello, World!")
  2. 处理数据类型的方式:
    • cout 能够自动推断输出对象的数据类型,并自动调用适当的输出运算符来进行输出。因此,它可以直接输出各种类型的数据,包括整数、浮点数、字符、字符串等。
    • printf 需要使用格式化字符串来指定输出的数据类型和格式,如 %d 表示整数,%f 表示浮点数,%c 表示字符等。
  3. 错误处理和安全:
    • cout 具有更好的错误处理机制,它提供了许多重载的输出运算符,可以检查输出操作是否成功,并能够自动刷新输出缓冲区。
    • printf 通常直接将数据写入输出缓冲区,如果需要手动刷新缓冲区,或者在输出过程中进行错误处理,可以使用相关的库函数来实现。
  4. 标准库支持:
    • cout 是 C++ 标准库的一部分,它提供了丰富的功能和灵活性,可以与其他标准库函数和类一起使用。
    • printf 是 C语言的标准库函数,它提供了一定的灵活性和功能,但相对于 cout 而言,它的功能相对较简单,且通常不与其他标准库函数和类一起使用。

#你知道重载运算符吗?

重载运算符是指在类中重新定义标准运算符的行为。通过重载运算符,可以使得类对象可以像内置类型一样进行运算操作,使得代码更加直观和易于理解。

  • 只能重载已有的运算符;并且符合其运算含义。如+-/*,><=,++/--
  • balabala

#当程序中有函数重载时,函数的匹配原则和顺序是什么?

匹配原则

Best_viable_function

  1. 根据函数名选定候选函数
    1. 其声明在调用处可见
  2. 根据实参选定可行函数
    1. 形参数量和实参一致
    2. 形参和实参类型相同,或能转换
  3. 根据匹配规则寻找最佳匹配
    1. 参数类型和个数完全一致
    2. 底层const转化
    3. 类型提升
    4. 算数类型转换
    5. 类类型转换
    6. 按顺序进行依次匹配
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void f(int *a) { cout << "const1" << endl; }
// redefinition of ‘void f(int*)’
// 形参只是临时量,调用结束就被销毁,所以是否是常量不会影响实参,可以搜索形参为什么忽略顶层const
// void f(int *const a) { cout << "const2" << endl; }
void f(const int *a) { cout << "const3" << endl; }

int main(int argc, char *argv[])
{
    int b = 4;
    int *a1 = &b;
    int *const a2 = &b;
    const int *a3 = &b;
    f(a1);  // 1
    f(a2);  // 1
    f(a3);  // 3
    return 0;
}

#定义和声明的区别

如果是指变量的声明和定义: 从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。

如果是指函数的声明和定义: 声明:一般在头文件里,只是让编译器知道这个函数的存在。 定义:一般在源文件里,存放函数的具体实现。

#全局变量和static变量的区别

按存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。

主要区别:

按作用域分,全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效。并且程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。

https://blog.csdn.net/mm_hh/article/details/77126878

static函数与普通函数有什么区别?

  • static函数与普通的函数作用域不同。静态函数只在本文件内可使用,普通函数可以被其他文件共享

  • static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。

#静态成员与普通成员的区别是什么?

  1. 生命周期

静态成员变量存储在静态存储区,其生命周期和程序的生命周期一致;

普通成员变量存储在堆或栈内,生命周期同创建的类的生命周期一致;

  1. 共享方式

静态成员变量是全类共享;普通成员变量是每个对象单独享用的;

  1. 初始化位置

普通成员变量在类中初始化;静态成员变量在类外初始化;

#说一下你理解的 ifdef endif代表着什么?

  1. 指的是条件编译,它让程序满足一定的条件下才会编译该代码段,否则不编译或者编译另一个代码段。

  2. 条件编译命令最常见的形式为:

1
2
3
4
5
#ifdef 标识符  
程序段1  
#else  
程序段2  
#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

  1. 在头文件使用,可以避免“重定义”错误。

#隐式转换,如何消除隐式转换?

隐式转换分为标准转换,用户自定义转换。标准准换即编译器内置的转换规则,如整数类型提升,数组退化成指针等。用户自定义转换包括转换构造函数,用于将其他类型转换为本类型,或者是自定义转换函数,用于将本类型转换为其他类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct A {};
 
struct B {
    // 转换构造函数
    B(int);
    B(const A&);
 
    // 用户定义转换函数,不需要显式指定返回值
    operator A();
    operator int();
}

对于需要进行隐式转换的上下文,编译器会生成一个隐式转换序列:

  1. 零个或一个由标准转换规则组成的标准转换序列,叫做初始标准转换序列
  2. 零个或一个由用户自定义的转换规则构成的用户定义转换序列
  3. 零个或一个由标准转换规则组成的标准转换序列,叫做第二标准转换序列

C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

https://www.cnblogs.com/apocelipes/p/14415033.html

#C++如何处理多个异常的?

http://c.biancheng.net/view/2331.html

使用try,throw,catch语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Base{ };
class Derived: public Base{ };

int main(){
    try{
        throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
        cout<<"Pass"<<endl;
    }catch(int){
        cout<<"Exception type: int"<<endl;
    }catch(char *){
        cout<<"Exception type: cahr *"<<endl;
    }catch(Base){  //匹配成功(向上转型)
        cout<<"Exception type: Base"<<endl;
    }catch(Derived){
        cout<<"Exception type: Derived"<<endl;
    }

    return 0;
}

#如何在不使用额外空间的情况下,交换两个数?你有几种方法

1
2
3
4
5
6
7
8
9
// 1) 算术
x = x + y;
y = x - y;
x = x - y; 

// 2) 异或
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;

#你知道strcpy和memcpy的区别是什么吗?

  • 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  • 复制的方法不同。strcpy不需要指定长度,它遇到被复制的字符串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。