面经-C语言基础(二)

目录

C 语言中全局变量、局部变量、函数参数个是在什么时候分配内存空间

const符号表机制

const的作用

大小端模式

关键字volatile有什么含意? 并给出三个不同的例子。

#include      和#include  “filename.h” 有什么区别?

const   有什么用途?(请至少说明两种)

如何引用一个已经定义过的全局变量?

带参宏与带参函数的区别(至少说出5点)?

数组名和指针的区别:

关键字inline必须与函数定义放在一起才能使函数成为内联

#ifndef和#pragma once

什么是可重入函数和不可重入函数(转)

C++ 虚继承实现原理(虚基类表指针与虚基类表)

计算机内部如何存储负数和浮点数?

函数调用的过程?

同步与异步,阻塞与非阻塞方式

1. 状态:

异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成;

2. 通知:

异步操作完成会给调用者发送特定信号;

3. 回调:

-异步操作完成时会调用回调函数。

4、线程阻塞的常见情况

c++STL vector扩容过程

1.5倍扩容优于2倍扩容的原因

new和malloc的区别/  自由存储区和堆的区别

c++之类内定义引用成员

lambda 表达式


 

C 语言中全局变量、局部变量、函数参数个是在什么时候分配内存空间

全局变量、全局静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;

局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化,且只分配一次。这里的变量包含内置数据类型和自定义类型的对象。

局部变量在定义时分配,超出作用域后释放。

函数参数与局部变量基本上相同在进入函数时分配,函数结束时释放。

const符号表机制

C语言中的const修饰的变量,

看起来不能修改,

其实可以通过指针变量,来间接修改,

const int a = 10;

int *p = NULL;

p = (int *)&a;

*p = 20;

cout<<a<<endl;

输出结果就是20,

注意这是C编译器中!!!

 

同样的代码,在C++中,就无法修改,

 

原因是什么呢?

C++中const修饰的变量,会放在一个符号表中,名值对中,写死了,

它可能分配内存空间,也可能不分配,

你再通过指针去间接修改,修改的就不是同一个。

c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。

 

 

 

const的作用

1.修饰变量

2.修饰参数

3.修饰函数返回 

 

class Rational { ... };

const Rational operator* (const Rational& lhs, const Rational& rhs);

这样当指向下列操作时就会编译出错。

 

Rational a, b, c;

(a * b) = c; //error

 

大小端模式

  • 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。符合我们的阅读顺序。
  • 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

判断方式:

1.用联合体

union A{

int a ;

char b;

};

A t;

t.a = 0x12345678;

if(t.b == 0x 78)

cout<<"小端";

 

2.强制类型转换

int i = 0x12345678;

char *p = (char*) & i ;

if(*p == 0x78 )

cout<< "小端";

else cout<<"大端";

关键字volatile有什么含意? 并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新(从内存中)读取这个变量的值,而不是使用保存在寄存器里的备份。下面volatile变量的几个例子:

1). 并行设备的硬件寄存器(如:状态寄存器)

2). 一个中断服务子程序中会访问到的非自动变量

(Non-automatic variables)

3). 多线程应用中被几个任务共享的变量

#include  <filename.h>    和#include  “filename.h” 有什么区别?

对于#include  <filename.h>   ,编译器从标准库路径开始搜索filename.h ;                                

对于#include  “filename.h” ,编译器从用户的工作路径开始搜索filename.h 。

const   有什么用途?(请至少说明两种)

(1)可以定义const   常量

(2)const  可以修饰函数的参数、返回值,甚至函数的定义体。被const  修饰的东西都受到强制保护,可

以预防意外的变动,能提高程序的健壮性。

如何引用一个已经定义过的全局变量?

可以用引用头文件的方式,也可以用extern 关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变量写错了,那么在编译期间会报错,如果你用extern 方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。

带参宏与带参函数的区别(至少说出5点)?

【标准答案】

                      带参宏                带参函数

处理时间             编译时                运行时

参数类型                 无                需定义

程序长度                变长                不变

占用存储空间           否                    是

运行时间           不占运行时间      调用和返回时占

数组名和指针的区别:

数组名确实表示指du向数组首地址的指针,zhi但这个指针很特别,它的值(指针的值指的dao是指针所指的地址)不能被改写,能改写的仅仅是其指向的内容,换句话说,数组名只能指向数组的首地址,如果有数组char a[];那么如果出现a = a+1;这是编译都通不过的错误。而对于一个普通的指针是可以的,再比如有数组char a[];那么再定义一个char *p = a;然后再用p = p+1是合法的,这表示让指针p指向&a[1]。它们的第二个区别是:每当用到数组名这个指针的时候,系统都会传入数组的信息,而普通的指针只是一个4字节的整数,例如:

char a[5];

char *p = a;//指针a和指针p都指向数组a的首地址

cout << sizeof (a) << "##" << sizeof (p) << endl;

这时的运行结果是“5##4”

 

 

a = b //不允许 不能作为左值。

 

关键字inline必须与函数定义放在一起才能使函数成为内联

定义声明最重要的区别:定义创建了对象并为这个对象分配了内存,声明没有分配内存。

即一个需要建立存储空间,如 int  i; 这是定义;而 extern int i; 是声明,并没有建立存储空间,只是告诉编译器该变量已经在别处定义过了。对于函数的定义和声明也同样如此,这就为什么内联 inline 函数只能在放在函数定义的前面,而不能放在声明之前。

#ifndef和#pragma once

#ifndef方式是C/C++语言的标准支持,也是比较常用的方式,#ifndef的方式依赖于自定义的宏名(例中的_CODE_BLOCK)不能冲突,它不光可以保证同一份文件不会被包含两次,也能够保证不同文件完全相同的内容不会被包含两次。但,同样的,如果自定义的宏名不小心“重名”了,两份不同的文件使用同一个宏名进行#ifndef,那么会导致编译器找不到声明的情况(被编译器判定为重定义而屏蔽了)。

此外,由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,#ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式(Visual Studio 2017新建头文件会自带#pragma once指令)。

#pragma once一般由编译器提供保证:同一个文件不会被包含多次。这里所说的”同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。无法对一个头文件中的一段代码作#pragma once声明,而只能针对文件。此方式不会出现宏名碰撞引发的奇怪问题,大型项目的编译速度也因此提供了一些。缺点是如果某个头文件有多份拷贝,此方法不能保证它们不被重复包含。在C/C++中,#pragma once是一个非标准但是被广泛支持的方式。

        #pragma once方式产生于#ifndef之后。#ifndef方式受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式有些编译器不支持(较老编译器不支持,如GCC 3.4版本之前不支持#pragmaonce),兼容性不够好。#ifndef可以针对一个文件中的部分代码,而#pragma once只能针对整个文件。相对而言,#ifndef更加灵活,兼容性好,#pragma once操作简单,效率高。

什么是可重入函数和不可重入函数(转)

     在 实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数不幸被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任 务调用这个函数的数据,从而导致不可预料的后果。那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会 出错。不可重入函数在实时系统设计中被视为不安全函数。

    满足下列条件的函数多数是不可重入的:

(1)函数体内使用了静态的数据结构;

(2)函数体内调用了malloc()或者free()函数;

(3)函数体内调用了标准I/O函数。

说法2

一个可重入的函数简单来说,就是:可以被中断的函数。就是说,你可以在这个函数执行的任何时候中断他的运行,在任务调度下去执行另外一段代 码而不会出现什么错误。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等等,所以他如果被中断的话,可能出现问题,所以这类函数是 不能运行在多任务环境下的。

 

printf()经常有重入解释

不可重入函数不可以在它还没有返回就再次被调用。例如printf,malloc,free等都是不可重入函数。因为中断可能在任何时候发生,例如在printf执行过程中,因此不能在中断处理函数里调用printf,否则printf将会被重入。

 

函数不可重入大多数是因为在函数中引用了全局变量。例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。

个人理解:如果中断发生的时候,当运行到printf的时候,假设发生了中断嵌套,而此时stdout资源被占用,所以第二个中断printf等待第一个中断的stdout资源释放,第一个中断等待第二个中断返回,造成了死锁,不知这样理解对不对。

C++ 虚继承实现原理(虚基类表指针与虚基类表)

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题。虚继承可以解决这个问题,

每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

1、D继承了B,C也就继承了两个虚基类指针

2、虚基类表存储的是,虚基类相对直接继承类的偏移(D并非是虚基类的直接继承类,B,C才是)

 

    #include<iostream> 

    using namespace std; 

     

    class A  //大小为4 

    { 

    public: 

        int a; 

    }; 

    class B :virtual public A  //大小为12,变量a,b共8字节,虚基类表指针4 

    { 

    public: 

        int b; 

    }; 

    class C :virtual public A //与B一样12 

    { 

    public: 

        int c; 

    }; 

    class D :public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针 

    { 

    public: 

        int d; 

    }; 

     

    int main() 

    { 

        A a; 

        B b; 

        C c; 

        D d; 

        cout << sizeof(a) << endl; 

        cout << sizeof(b) << endl; 

        cout << sizeof(c) << endl; 

        cout << sizeof(d) << endl; 

        system("pause"); 

        return 0; 

    } 

 

计算机内部如何存储负数和浮点数?

负数比较容易,就是通过一个标志位和补码来表示。

对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。

无论是单精度还是双精度在存储中都分为三个部分:

  • 1). 符号位(Sign) : 0代表正,1代表为负
  • 2). 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
  • 3). 尾数部分(Mantissa):尾数部分
    其中float的存储方式如下图所示:

而双精度的存储方式如下图:

 

函数调用的过程?

如下结构的代码,

int main(void)
{
  ...
  d = fun(a, b, c);
  cout<<d<<endl;
  ...
  return 0;
}

调用fun()的过程大致如下:

  • main()========
  • 1).参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
  • 2).保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位置);
  • 3).跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
  • fun()=====
  • 4).移动ebp、esp形成新的栈帧结构;
  • 5).压栈(push)形成临时变量并执行相关操作;
  • 6).return一个值;
  • 7).出栈(pop);
  • 8).恢复main函数的栈帧结构;
  • 9).返回main函数;
  • main()========

 

ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

同步与异步,阻塞与非阻塞方式

下面介绍同步,异步,阻塞,非阻塞这几个概念,加深对多线程编程的理解。

有了之前的概念,我们可以想象,当几个线程或者进程在并发执行时,如果我们不加任何干预措施,那么他们的执行顺序是由系统当时的环境来决定的,所以不同时间段不同环境下运行的顺序都会不尽相同,这便是异步(有差异的步骤)。当然,同步肯定就是通过一定的措施,使得几个线程或者进程总是按照一定顺序来执行(总是按照相同的步骤)。

当一个进程或者线程请求某一个资源而不得时,如I/O,便会进入阻塞状态,一直等待。scanf()便是一个很好的例子,当程序运行到scanf()时,如果输入缓存区为空,那么程序便会进入阻塞状态等待我们从键盘输入,这便是以阻塞的方式调用scanf()。通过一定方法,我们可以将scanf()变成非阻塞的方式来执行。如给scanf()设置一个超时时间,如果时间到了还是没有输入那么便跳过scanf(),这个时候我们就称为用非阻塞的方式来调用scanf()。

对比可以发现,同步即阻塞。想要按照某特定顺序来执行一系列过程,在上一个过程完成之前下一个过程必须等待,这就是阻塞在了这个地方。当同步运行的时候,会等待同步操作完成才会返回,否则会一直阻塞在同步操作处。

相反的,异步即非阻塞,当异步调用某个函数时,函数会立刻返回,而不会阻塞在那。

怎么判断异步操作是否已经完成?通常有3种方式:

1. 状态:

异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成;

2. 通知:

异步操作完成会给调用者发送特定信号;

3. 回调:

-异步操作完成时会调用回调函数。

所以同步即阻塞,异步即非阻塞。

4、线程阻塞的常见情况

1. 调用sleep()进入睡眠状态;

2. 用wait()暂停了线程,除非收到notify()唤醒线程;

3. 线程正在等待一些IO操作;

4. 线程正在试图调用被锁起来了的对象。

c++STL vector扩容过程

扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间,并且,由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。

1.5倍扩容优于2倍扩容的原因

  • 两倍扩容
    假设我们一开始申请了 16Byte 的空间。
    当需要更多空间的时候,将首先申请 32Byte,然后释放掉之前的 16Byte。这释放掉的16Byte 的空间就闲置在了内存中。
    当还需要更多空间的时候,你将首先申请 64Byte,然后释放掉之前的 32Byte。这将在内存中留下一个48Byte 的闲置空间(假定之前的 16Byte 和此时释放的32Byte 合并)
    当还需要更多空间的时候,你将首先申请128Byte,然后释放掉之前的 64 Byte。这将在内存中留下一个112Byte 的闲置空间(假定所有之前释放的空间都合并成了一个块)

扩容因子为2时,上述例子表明:每次扩容,我们释放掉的内存连接起来的大小,都小于即将要分配的内存大小。

  • 1.5倍扩容

假设我们一开始申请了 16Byte 的空间。

当需要更多空间的时候,将申请 24 Byte ,然后释放掉 16 ,在内存中留下 16Byte 的空闲空间。

当需要更多空间的时候,将申请 36 Byte,然后释放掉 24,在内存中留下 40Byte (16 + 24)的空闲空间。

当需要更多空间的时候,将申请 54 Byte,然后释放 36,在内存中留下 76Byte。

当需要更多空间的时候,将申请 81 Byte,然后释放 54, 在内存中留下 130Byte。

当需要更多空间的时候,将申请 122 Byte 的空间(复用内存中闲置的 130Byte)

new和malloc的区别/  自由存储区和堆的区别

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

c++之类内定义引用成员

c++类内可以定义引用成员变量,但要遵循以下三个规则:

1、不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误。

2、构造函数的形参也必须是引用类型

3、不能在构造函数里初始化,必须在初始化列表中进行初始化。

lambda 表达式

  1. 形式

[ ] ( ){ }

[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

  1.  

[ ] 可为 = , & ,  a ,  & a , =  &a &b

( )  mutable 表示传进来的值可以被修改   [m] (mutable) { m = 100 + 10};

  1.  

返回值 int ret = []() ->int {return 1000; }(); [ ] (int x, int y) ->int { int z = x + y; return z}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值