面试基础概念题(keep updating)

发布时间 2023-05-04 20:22:55作者: 英俊的萌萌

1、const的作用有哪些,谈一谈你对const的理解?
(1)const起一个限制作用,限制修改,防止被修饰的成员的内容被改变。使用const关键字修饰的变量可以认为有只读属性。

(2)const 关键字修饰函数形参时,可以保护输入的参数。(如 ,字符串拷贝函数 : char *strcpy(char *strDest, const char *strSrc) , 其中const char *strSrc 为常量指针,const 修饰的是 *strSrc , 所以 *strSrc代表的内容不能被修改。)

(3)const修饰函数的返回值的时候,可以保护指针指向的内容或者引用的内容不被修改。(比如:const char * function(void);)

(4)const 修饰指针变量的时候 , 可以构成常量指针与指针常量两种容易混淆的概念。

2、描述char、const char、char* const、const char* const的区别?
A: char 定义普通字符,const char 定义的字符可以认为有只读属性,char *const 修饰的变量为指针常量,const修饰的是那个指向该字符的指针,所以该指针不能被改变指向,但是可以改变该指针当前所指的字符内容。

const char * const (常量指针常量), 既不能改变指针所指的内容,也不能改变指针的指向。

3、指针常量和常量指针有什么区别?
指针常量:char * const p; // 指的是p这个指针被声明为常量,不能改变指针的指向,但是可以改变指针指向内存中的内容。
常量指针: char const * p;// 指的时p指针指向的内容被声明为常量,不能改变指针指向的内容,但是可以改变指针的指向。

4、static的作用是什么,什么情况下用到static?
1)在函数内部,static 关键字可以用来控制函数的作用域。静态局部变量的生命周期和全局变量相同,但是其作用域被限制在了函数内部。
2)在文件级别(也就是在函数外部),static 关键字可以用来限制变量或函数的作用域,这种变量被称为静态全局变量。静态全局变量的生命周期和全局变量相同,但是其作用域被限制在了当前文件内部。
3)在类中,static 关键字可以用来声明静态成员变量和静态成员函数。静态成员变量是属于类的变量,而不是属于类的实例的变量。静态成员函数是属于类的函数,而不是属于类的实例的函数。静态成员变量和静态成员函数可以通过类名访问,也可以通过对象名访问。

5、全局变量与局部变量的区别?
全局变量和局部变量都是存储数据的方式,但其作用域和生命周期不同。全局变量在整个程序中都可以访问,生命周期也是整个程序的运行周期。而局部变量只在其定义的函数内部可用,生命周期也只在函数执行时存在。

6、宏定义的作用是什么?
宏定义是一种预处理指令,只是简单的文本替换,预处理器只是简单地将代码中出现的宏名替换为宏定义中的内容,可以在程序中定义一个常量或者一个简单的函数。宏定义可以提高程序的可读性,很重要的一个作用就是避免程序中出现大量的魔法数字或者重复代码。

7、内存对齐的概念?为什么会有内存对齐?
内存对齐是指在内存中分配空间时,按照一定规则将数据排列在内存中的过程。在现代计算机中,内存对齐是为了提高数据访问的效率。
1)CPU对内存的读取不是连续的,而是分成一块一块大小来读,块的大小只能是1、2、4、8、16...字节。
2)当读取操作的数据不对齐,CPU需要两次总线周期来访问内存,数据对齐的情况下只需要一次,因此不对齐的话性能会有折扣。
具体对齐规则如下:
//1、数据成员本身要对齐,每个数据成员都要对齐
//2、结构体也要对齐,最后结构体的大小是结构体中最大数据成员的整数倍。
//3、结构体里面还有结构体的时候,里面的结构体要对齐,并且里面结构体也要满足对齐规则(要按照最大数据成员的整数倍开始对齐。)
除此之外,对齐的进行还跟对齐系数(pragma pack)相关,比如64位系统默认对齐系数为8,32位系统默认对齐系数只有4。通过预编译(#pragma pack(n), n= 1,2,4,8,16)指令可以改变对齐系数。

8、inline 内联函数的特点有哪些?它的优缺点是什么?
内联函数是一种特殊的函数,它在编译时会将函数体直接嵌入到调用代码中,而不是通过函数调用来执行。内联函数的特点有:
1)编译器会将内联函数的代码直接嵌入到调用代码中,减少了函数调用的开销,从而提高了程序的执行效率。
2)内联函数通常是在头文件中定义的,这样可以让编译器在编译时直接将函数的代码嵌入到调用代码中,避免了多次链接相同的函数代码。
3)内联函数的调用方式和普通函数相同,对程序员来说是透明的,只需要在函数声明前加上 inline 关键字即可。

内联函数的优点:
1)提高程序的执行效率且减少了代码体积:代码会被直接嵌入到调用代码中,避免了函数调用的开销,从而提高了程序的执行效率,减少了代码体积。
2)方便调试:内联函数的代码在调用处展开,可以方便地进行调试。

内联函数的缺点:
1)增加代码体积:内联函数的代码会被直接嵌入到调用代码中,如果内联函数的代码比较长,会使得代码体积增大,从而降低程序的性能。
2)编译时间增加:内联函数的代码会被直接嵌入到调用代码中,如果内联函数的使用比较频繁,会增加编译时间
3)可读性降低:内联函数的代码会被直接嵌入到调用代码中,使得代码可读性降低,从而增加了代码维护的难度

9、如何用C 实现 C++ 的面向对象特性(封装、继承、多态)

1)封装:在 C 中,可以通过结构体和函数指针来实现类似于封装的效果。
首先,定义一个结构体来表示一个对象的数据:

typedef struct {
    int data;
} Object;

然后,定义一组函数来操作这个结构体,这些函数可以看作是对象的行为:

void Object_init(Object *obj, int data) {
    obj->data = data;
}

int Object_get_data(Object *obj) {
    return obj->data;
}

void Object_set_data(Object *obj, int data) {
    obj->data = data;
}

这些函数可以通过函数指针来封装到一个结构体中:

typedef struct {
    void (*init)(Object *, int);
    int (*get_data)(Object *);
    void (*set_data)(Object *, int);
} Object_vtable;

typedef struct {
    Object_vtable *vtable;
    Object data;
} Encapsulation;

void Encapsulation_init(Encapsulation *encap, int data) {
    encap->vtable->init(&encap->data, data);
}

int Encapsulation_get_data(Encapsulation *encap) {
    return encap->vtable->get_data(&encap->data);
}

void Encapsulation_set_data(Encapsulation *encap, int data) {
    encap->vtable->set_data(&encap->data, data);
}

现在,我们就可以使用 Encapsulation 结构体来实现类似于封装的效果了:

int main() {
    Object_vtable obj_vtable = {
        .init = Object_init,
        .get_data = Object_get_data,
        .set_data = Object_set_data
    };
    Encapsulation encap = {
        .vtable = &obj_vtable
    };
    Encapsulation_init(&encap, 42);
    printf("%d\n", Encapsulation_get_data(&encap));  // 输出 42
    Encapsulation_set_data(&encap, 100);
    printf("%d\n", Encapsulation_get_data(&encap));  // 输出 100
    return 0;
}

2)继承
在 C 中,可以通过结构体嵌套和函数指针来实现类似于继承的效果。
首先,定义一个基类的结构体和函数指针:

typedef struct {
    int base_data;
} Base;

typedef struct {
    void (*base_method)(Base *);
} Base_vtable;

然后,定义一个派生类的结构体,它嵌套了一个基类的结构体,并且定义了一个自己的数据和方法:

typedef struct {
    Base_vtable *base_vtable;
    int derived_data;
} Derived;

void Derived_method(Derived *obj) {
    printf("Derived_method: base_data=%d, derived_data=%d\n", obj->base_vtable->base_method, obj->derived_data);
}

Derived_vtable Derived_vtable_instance = {
    .base_vtable = &Base_vtable_instance,
    .derived_method = Derived_method
};

现在,我们就可以定义一个 Derived 对象,并且调用它的方法了:

int main() {
    Base_vtable Base_vtable_instance = {
        .base_method = NULL
    };
    Derived_vtable Derived_vtable_instance = {
        .base_vtable = &Base_vtable_instance,
        .derived_method = Derived_method
    };
    Derived obj = {
        .base_vtable = &Derived_vtable_instance,
        .derived_data = 42
    };
    obj.base_vtable->base_method = (void (*)(Base *))Derived_method;
    obj.base_data = 100;
    obj.base_vtable->base_method((Base *)&obj);  // 输出 Derived_method: base_data=100, derived_data=42
    return 0;
}

这里需要注意的是,如果派生类的方法需要访问基类的数据或方法,可以通过基类的指针来实现。

3)多态
在 C 中,可以通过函数指针和虚函数表来实现类似于多态的效果。
首先,定义一个基类的结构体和虚函数表:

typedef struct {
    void (*virtual_method)(void *);
} Base;

typedef struct {
    void (*virtual_method)(void *);
} Base_vtable;

然后,定义一个派生类的结构体,它嵌套了一个基类的结构体,并且定义了一个自己的虚函数表:

typedef struct {
    Base base;
    int derived_data;
} Derived;

void Derived_method(Derived *obj) {
    printf("Derived_method: derived_data=%d\n", obj->derived_data);
}

Derived_vtable Derived_vtable_instance = {
    .virtual_method = (void (*)(void *))Derived_method
};

现在,我们就可以定义一个 Derived 对象,并且调用它的虚函数了:

int main() {
    Derived obj = {
        .base = {
            .virtual_method = (void (*)(void *))Derived_method
        },
        .derived_data = 42
    };
    obj.base.virtual_method((void *)&obj);  // 输出 Derived_method: derived_data=42
    return 0;
}

这里需要注意的是,如果派生类的虚函数需要访问派生类的数据,可以通过派生类的指针来实现。此外,虚函数表必须在每个对象中都存在,这会增加内存的开销。

10、memcpy怎么实现让它效率更高?
memcpy 是一个 C 标准库函数,用于在内存之间复制指定数量的字节。为了实现更高效的 memcpy,可以采用以下几种方法:

1)使用平台特定的指令集。现代的 CPU 都支持一些特定的指令集,例如 SSE、AVX 等。这些指令集可以对数据进行并行操作,从而提高复制速度。

2)使用编译器优化。现代的编译器都可以对代码进行优化,使得代码更加高效。例如,可以使用 -O2 或 -O3 选项来启用编译器优化。另外,可以使用 restrict 关键字来告诉编译器,指针之间没有重叠,从而使得编译器可以进行更好的优化。

3)使用多线程。在多核 CPU 上,可以使用多线程来并行复制数据,从而加快复制速度。例如,可以将数据划分为多个块,每个线程负责复制一个块,然后将结果合并。需要注意的是,多线程的实现需要考虑线程同步和数据竞争等问题。参考代码如下:

#include <pthread.h>

typedef struct {
    void* dest;
    const void* src;
    size_t n;
} memcpy_arg;

void* memcpy_worker(void* arg) {
    memcpy_arg* m_arg = (memcpy_arg*)arg;
    memcpy(m_arg->dest, m_arg->src, m_arg->n);
    return NULL;
}

void* my_memcpy(void* dest, const void* src, size_t n) {
    const size_t num_threads = 4;  // 使用 4 个线程
    size_t i, block_size = n / num_threads;
    pthread_t threads[num_threads];
    memcpy_arg args[num_threads];

    for (i = 0; i < num_threads; i++) {
        args[i].dest = (char*)dest + i * block_size;
        args[i].src = (const char*)src + i * block_size;
        args[i].n = (i == num_threads - 1) ? n - i * block_size : block_size;
        pthread_create(&threads[i], NULL, memcpy_worker, &args[i]);
    }

    for (i = 0; i < num_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    return dest;
}

需要注意的是,多线程的实现可能会导致额外的开销,因此在数据比较小的情况下,多线程实现可能不如单线程实现效率高。