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;
}
需要注意的是,多线程的实现可能会导致额外的开销,因此在数据比较小的情况下,多线程实现可能不如单线程实现效率高。
typedef和define有什么区别?'
(1) 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2) 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3) 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4) 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
extern有什么作用,extern C有什么作用?
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
如何避免野指针?
“野指针”产生原因及解决办法如下:
(1) 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
(2) 指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
(3) 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。
如何计算结构体长度?
sizeof和strlen有什么区别?
sizeof 和 strlen 有以下区别:
1 sizeof 是一个操作符,strlen 是库函数。
2 sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字符串作参数。
3 编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof 计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
4 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是 sizeof。
知道条件变量吗?条件变量为什么要和锁配合使用?
C语言和C++有什么区别?
struct和class有什么区别?
extern "C"的作用?
函数重载和覆盖有什么区别?
谈一谈你对多态的理解,运行时多态的实现原理是什么?
多态:就是多种形态,C++的多态分为静态多态和动态多态。静态多态就是重载,因为在编译器决议确定,所以成为静态多态。在编译时就可以确定函数地址。动态多态即运行时多态是通过继承重写基类的虚函数实现的多态,因为在运行时决议确定,所以称为动态多态,也叫运行时多态。运行时在虚函数表中寻找调用函数的地址。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是子类,就调用子类的函数。如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。
运行时多态实现原理:
1)用 virtual 关键字声明的函数叫虚函数,虚函数肯定是类的成员函数。
2)存在虚函数的类都有一个一维的虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
3)类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
4)虚函数表是一个存储类成员函数指针的数据结构。
5)虚函数表是由编译器自动生成和维护的。
6)virtual 声明的成员函数会被编译器放入虚函数表中。
7)当存在虚函数时,每个对象都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当执行虚函数时,C++编译器不需要区分子类或父类对象,只需在基类之恨或引用中找到vptr指针即可。
8)ptr一般作为类对象的第一个成员。
对虚函数机制的理解,单继承、多继承、虚继承条件下虚函数表的结构
如果虚函数是有效的,那为什么不把所有函数设为虚函数?
虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候会产生一定的系统开销,这是没有必要的。另外,虚函数的调用相对于普通函数要更慢一些,因此每次都要查找虚函数表,有一定的时间开销。
构造函数可以是虚函数吗?析构函数可以是虚函数吗?
什么场景需要用到纯虚函数?纯虚函数的作用是什么?
在许多情况下,在基类中不能对虚函数给出有意义的的实现,而把它定义为纯虚函数,它的实现留给派生类去做。作用:1.为了方便使用多态特性。2.在很多情况下,基类本身生成对象时不合情理的。
浙公网安备 33010602011771号