目录
C 语言中全局变量、局部变量、函数参数个是在什么时候分配内存空间
#include 和#include “filename.h” 有什么区别?
异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成;
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 表达式
- 形式
[ ] ( ){ }
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
[ ] 可为 = , & , a , & a , = &a &b
( ) mutable 表示传进来的值可以被修改 [m] (mutable) { m = 100 + 10};
返回值 int ret = []() ->int {return 1000; }(); [ ] (int x, int y) ->int { int z = x + y; return z}