//构造N个结点的单链表返回链表头指针,要求链表中各结点顺序
//与结点数据输入顺序相反,例如输入1,2,3,4,5,形成的链表为
//head->5 4 3 2 1 ,补充程序
#define N 10
typedef struct Node
{
int data;
struct Node*next;
}NODE;
int Get_Data(int i);//定义省略
Node*Create_u()
{
int i;
NODE*p,*Head=NULL;
for(i=0;i { VP=New NODE; P->Data=Get_Data(i); ___ p->next = Head->next; ______ Head =p _____; } return Head; } //7********************************************** //N个结点链表,每个结点中存放一个字符,判断链表存放的字符是否 //中心对称,即a b c c b a或a b c b a,补充程序 typedef struct Node { int data; struct Node*next; }NODE; bool Is_symmeic(NODE*head,*int n) { char D[N]; int i,d; _____d=n/2 ___; for(i=0;i { D[i]=head->data; head=head->next; } if(_____head!=NULL&&_ 1==n%2___) { head=head->next; } while(head) { _____ --i __________; if(D[i]!=head->data) { return false; } head=head->next; } return true; } //8************************************* //str中只含有大写和小写字母函数change_move(char*str)将字符串中大写改成*并 //移到前面小写后返回*的个数 //如AabBdcYY改为*****abd,返回5 int chang_move(char*str) { int len,i,curstr=-1; len=strlen(str); for(i=len-1;i>=0;i--) { if(str[i]>='A'&&str[i]<='Z') { str[i]='*'; if(cursor==-1) { cursor=i; } else if(cursor>i) { _____________; str[i]='*'; _____________; } } return____________; } //9*********************************************** //求两个字符串的第一个公共子串,并返回该子串 //如:"a b c d e f g e h i" "a a c d e f * * g e h i" //第一个为"c d e f";不许用strcmp() char*Maxf(char*str1,char*str2) { } 3.二维数组空间的动态申请 a.简单的,已经有一维,如 char (*c)[5]; c=new char[n][5];//n为已定义的行数 b.二维的 int **p; p=new int*[m_row];//创建行指针 for(int i=0;i p[i]=new int[m_cols]; 写到某一个函数中: void getmemory(int ** &p,int m_row,int m_cols) { p=new int*[m_row];//创建行指针 for(int i=0;i p[i]=new int[m_cols]; } 释放空间: void deletememory(int **&p,int m_row) { //释放每一行所分配的空间 for(int i=0;i delete [] x[i]; //释放行指针 delete [] x; x=0; via嵌入式笔试两题 -|yingwang294 发表于 2006-10-31 10:40:00 以下是威盛嵌入式笔试的最后两道小题 题一: 原题如下:改程序,并确定运行结果 #i nclude #i nclude #i nclude char *getstring(void) { char p[]="hello everyone"; return p; } char *getmemory(void) { return (char *)malloc(10); } int main(int argc, char* argv[]) { char *p=getmemory(); strcpy(p,"helloworld"); printf("%s",p); printf("%s",getstring()); return 0; } 这个主要是char p[]前少一个static... 题二: 读程序,写出运行结果 #i nclude #i nclude #i nclude #i nclude typedef struct { int value; char type; }head_t;这是什么东西啊? typedef struct { head_t head; int para; }message_t; void main(void) { message_t *message=NULL; head_t *head=NULL; message=(message_t *)malloc(sizeof(message_t)); assert(message);//测试的条件不成立则终止程序 memset(message,0,sizeof(message_t)); message->para=100; message->head.type='a'; head=(head_t *)message; head->value++; head->type++; printf("message->head.value=%d,message->head.type=%c,message->para=%d\\n",me ssage->head.value,message->head.type,message->para); " free(message); return; } #include #include using namespace std; class Student { public: Student() {} Student( const string& nm, int sc = 0 ) : name( nm ), score( sc ) {} void set_student( const string& nm, int sc = 0 ) { name = nm; score = sc; } const string& get_name() const { return name; } int get_score() const { return score; } private: string name; int score; }; // output student's name and score void output_student( const Student& student ) { cout << student.get_name() << "\"; cout << student.get_score() << endl; } int main() { Student stu( "Wang", 85 ); output_student( stu ); } 设 计了一个类 Student,数据成员有 name 和 score,有两个构造函数,有一个设置成员数据函数 set_student(),各有一个取得 name 和 score 的函数 get_name() 和 get_score()。请注意 get_name() 和 get_score() 后面都加了 const,而 set_student() 后面没有(也不能有const)。 首先说一点题外话,为什么 get_name() 前面也加 const。如果没有前后两个 const 的话,get_name() 返回的是对私有数据成员 name 的引用,所以通过这个引用可以改变私有成员 name 的值,如 Student stu( "Wang", 85 ); stu.get_name() = "Li"; 即把 name 由原来的 "Wang" 变成了 "Li",而这不是我们希望的发生的。所以在 get_name() 前面加 const 避免这种情况的发生。 那么,get_name() 和 get_score() 这两个后面应该加 const的成员函数,如果没有 const 修饰的话可不可以呢?回答是可以!但是这样做的代价是:const对象将不能再调用这两个非const成员函数了。如 const string& get_name(); // 这两个函数都应该设成 const 型 int get_score(); void output_student( const Student& student ) { cout << student.get_name() << "\"; // 如果 get_name() 和 get_score() 是非const成员函数,这一句和下一句调用是错误的 cout << student.get_score() << endl; } 由 于参数student表示的是一个对const Student型对象的引用,所以 student 不能调用非const成员函数如 set_student()。如果 get_name() 和 get_score() 成员函数也变成非const型,那么上面的 student.get_name() 和 student.get_score() 的使用就是非法的,这样就会给我们处理问题造成困难。 因此,我们没有理由反对使用const,该加const时就应该加上const,这样使成员函数除了非const的对象之外,const对象也能够调用它。 c/C++ 通用 Makefile 本文提供了一个用于对 C/C++ 程序进行编译和连接以产生可执行程序的通用 Makefile。 在使用 Makefile 之前,只需对它进行一些简单的设置即可;而且一经设置,即使以后对源程序文件有所增减一般也不再需要改动 Makefile。因此,即便是一个没有学习过 Makefile 书写规则的人,也可以为自己的 C/C++ 程序快速建立一个可工作的 Makefile。 这个 Makefile 可以在 GNU Make 和 GCC 编译器下正常工作。但是不能保证对于其它版本的 Make 和编译器也能正常工作。 如果你发现了本文中的错误,或者对本文有什么感想或建议,可通过 whyglinux AT hotmail DOT com 邮箱和作者联系。 此 Makefile 的使用方法如下: 1.程序目录的组织 尽量将自己的源程序集中在一个目录中,并且把 Makefile 和源程序放在一起,这样用起来比较方便。当然,也可以将源程序分类存放在不同的目录中。 在程序目录中创建一个名为 Makefile 的文本文件,将后面列出的 Makefile 的内容复制到这个文件中。(注意:在复制的过程中,Makfile 中各命令前面的 Tab 字符有可能被转换成若干个空格。这种情况下需要把 Makefile 命令前面的这些空格替换为一个 Tab。) 将当前工作目录切换到 Makefile 所在的目录。目前,这个 Makefile 只支持在当前目录中的调用,不支持当前目录和 Makefile 所在的路径不是同一目录的情况。 2.指定可执行文件 程序编译和连接成功后产生的可执行文件在 Makefile 中的 PROGRAM 变量中设定。这一项不能为空。为自己程序的可执行文件起一个有意义的名子吧。 3.指定源程序 要编译的源程序由其所在的路径和文件的扩展名两项来确定。由于头文件是通过包含来使用的,所以在这里说的源程序不应包含头文件。 程序所在的路径在 SRCDIRS 中设定。如果源程序分布在不同的目录中,那么需要在 SRCDIRS 中一一指定,并且路径名之间用空格分隔。 4.Makefile 目标(Targets) 下面是关于这个 Makefile 提供的目标以及它所完成的功能: omake 编译和连接程序。相当于 make all。 omake objs 仅仅编译程序产生 .o 目标文件,不进行连接(一般很少单独使用)。 omake clean 删除编译产生的目标文件和依赖文件。 omake cleanall 删除目标文件、依赖文件以及可执行文件。 omake rebuild 重新编译和连接程序。相当于 make clean && make all。 下面提供两个例子来具体说明上面 Makefile 的用法。 例一 Hello World 程序 这个程序的功能是输出 Hello, world! 这样一行文字。由 hello.h、hello.c、main.cxx 三个文件组成。前两个文件是 C 程序,后一个是 C++ 程序,因此这是一个 C 和 C++ 混编程序。 * C header file */ #ifndef HELLO_H #define HELLO_H #ifdef __cplusplus extern "C" { #endif void print_hello(); #ifdef __cplusplus } #endif #endif * C source file. */ #include "hello.h" #include void print_hello() { puts( "Hello, world!" ); } * C++ source file. */ #include "hello.h" int main() { print_hello(); return 0; } 建立一个新的目录,然后把这三个文件拷贝到目录中,也把 Makefile 文件拷贝到目录中。之后,对 Makefile 的相关项目进行如下设置: 代码 SRCDIRS := . # 源程序位于当前目录下 SRCEXTS := .c .cxx # 源程序文件有 .c 和 .cxx 两种类型 CFLAGS := -g # 为 C 目标程序包含 GDB 可用的调试信息 CXXFLAGS := -g # 为 C++ 目标程序包含 GDB 可用的调试信息 由于这个简单的程序只使用了 C 标准库的函数(puts),所以对于 CFLAGS 和 CXXFLAGS 没有过多的要求,LDFLAGS 和 CPPFLAGS 选项也无需设置。 经过上面的设置之后,执行 make 命令就可以编译程序了。如果没有错误出现的话,./hello 就可以运行程序了。 如果修改了源程序的话,可以看到只有和修改有关的源文件被编译。也可以再为程序添加新的源文件,只要它们的扩展名是已经在 Makefile 中设置过的,那么就没有必要修改 Makefile。 C/C++程序员应聘试题剖析 1.引言 本文的写作目的并不在于提供C/C++程序员求职面试指导,而旨在从技术上分析面试题的内涵。文中的大多数面试题来自各大论坛,部分试题解答也参考了网友的意见。 许多面试题看似简单,却需要深厚的基本功才能给出完美的解答。企业要求面试者写一个最简单的strcpy函数都可看出面试者在技术上究竟达到了怎样的程度,我们能真正写好一个strcpy函数吗?我们都觉得自己能,可是我们写出的strcpy很可能只能拿到10分中的2分。读者可从本文看到strcpy函数从2分到10分解答的例子,看看自己属于什么样的层次。此外,还有一些面试题考查面试者敏捷的思维能力。 分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。 2.找错题 试题1: void test1() { char string[10]; char* str1 = "01234567"; strcpy( string, str1 ); 试题2: void test2() { char string[10], str1[10]; int i; for(i=0; i<10; i++) { str1[i] = 'a'; } strcpy( string, str1 ); 试题3: void test3(char* str1) { char string[10]; if( strlen( str1 ) <= 10 ) { strcpy( string, str1 ); } 解答: 试题1字符串str1需要11个字节才能存放下(包括末尾的’\\0’),而string只有10个字节的空间,strcpy会导致数组越界; 对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分; 对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’\0’所占用的1个字节。 剖析: 考查对基本功的掌握: (1)字符串以’\0’结尾; (2)对数组越界把握的敏感度; (3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案: 2分 void strcpy( char *strDest, char *strSrc ) { while( (*strDest++ = * strSrc++) != ‘\0’ ); 4分 void strcpy( char *strDest, const char *strSrc ) //将源字符串加const,表明其为输入参数,加2分 { while( (*strDest++ = * strSrc++) != ‘\0’ ); 7分 void strcpy(char *strDest, const char *strSrc) { //对源地址和目的地址加非0断言,加3分 assert( (strDest != NULL) && (strSrc != NULL) ); while( (*strDest++ = * strSrc++) != ‘\0’ ); 10分 //为了实现链式操作,将目的地址返回,加3分! char * strcpy( char *strDest, const char *strSrc ) { assert( (strDest != NULL) && (strSrc != NULL) ); char *address = strDest; while( (*strDest++ = * strSrc++) != ‘\0’ ); return address; 从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊! (4)对strlen的掌握,它没有包括字符串末尾的'\\0'。 读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const { assert( strt != NULL ); //断言字符串地址非0 int len; while( (*str++) != '\\0' ) { len++; } return len; 试题4: void GetMemory( char *p ) { p = (char *) malloc( 100 );没有返回,错 } void Test( void ) { char *str = NULL; GetMemory( str ); strcpy( str, "hello world" ); printf( str ); 试题5: char *GetMemory( void ) { char p[] = "hello world"; return p;返回栈区局部变量的地址,错 } void Test( void ) { char *str = NULL; str = GetMemory(); printf( str ); 试题6: void GetMemory( char **p, int num ) { *p = (char *) malloc( num ); } void Test( void ) { char *str = NULL; GetMemory( &str, 100 ); strcpy( str, "hello" ); printf( str ); 试题7: void Test( void ) { char *str = (char *) malloc( 100 ); strcpy( str, "hello" ); free( str ); ... //省略的其它语句 解答: 试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完 char *str = NULL; 后的str仍然为NULL;那应该如何改????????????返回return、 试题5中 char p[] = "hello world"; 的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。 试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句 后未判断内存是否申请成功,应加上: if ( *p == NULL ) { ...//进行申请内存失败处理 试题7存在与试题6同样的问题,在执行 后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,???????应加上: 试题6的Test函数中也未对malloc的内存进行释放。 剖析: 试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。 对内存操作的考查主要集中在: (1)指针的理解; (2)变量的生存期及作用范围; (3)良好的动态内存申请和释放习惯。 再看看下面的一段程序有什么错误: swap( int* p1,int* p2 ) { int *p;// p是一个“野”指针 *p = *p1; *p1 = *p2; *p2 = *p; 在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为: swap( int* p1,int* p2 ) { int p; p = *p1; *p1 = *p2; *p2 = p; 3.内功题 试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var) 解答: BOOL型变量:if(!var) int型变量: if(var==0) float型变量: const float EPSINON = 0.000001; if ((x >= - EPSINON) && (x <= EPSINON) 指针变量: if(var==NULL) 剖析: 考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。 一般的,如果想让if判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。 浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则判为错,得0分。 试题2:以下为Windows NT下的32位C++程序,请计算sizeof的值 void Func ( char str[100] ) { sizeof( str ) = ? } void *p = malloc( 100 ); 解答: sizeof( str ) = 4 剖析: Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。 数组名的本质如下: (1)数组名指代一种数据结构,这种数据结构就是数组; 例如: char str[10]; 输出结果为10,str指代数据结构char[10]。 (2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改; char str[10]; str++; //编译出错,提示str不是左值 // (3)数组名作为函数形参时,沦为普通指针。 Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。 试题3:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事? 解答: MIN(*p++, b)会产生宏的副作用 剖析: 这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。 程序员对宏定义的使用要非常小心,特别要注意两个问题: (1)谨慎地将宏定义中的“参数”和整个宏用括弧括起来。所以,严格地讲,下述解答: #define MIN(A,B) (A) <= (B) ? (A) : (B) 都应判0分; (2)防止宏的副作用。 宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是: ((*p++) <= (b) ? (*p++) : (*p++)) 这个表达式会产生副作用,指针p会作三次++自增操作。 试题4:为什么标准头文件都有类似以下的结构? #ifndef __INCvxWorksh #define __INCvxWorksh #ifdef __cplusplus extern "C" { #endif /*...*/ #ifdef __cplusplus } #endif 解答: 头文件中的编译宏 #ifndef __INCvxWorksh #define __INCvxWorksh 的作用是防止被重复引用。 作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在symbol库中的名字与C语言的不同。例如,假设某个函数的原型为: 该函数被C编译器编译后在symbol库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。_foo_int_int这样的名字包含了函数名和函数参数数量及类型信息,C++就是考这种机制来实现函数重载的。 为了实现C和C++的混合编程,C++提供了C连接交换指定符号extern "C"来解决名字匹配问题,函数声明前加上extern "C"后,则编译器就会按照C语言的方式将该函数编译为_foo,这样C语言中就可以调用C++的函数了。 试题5:编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” 函数头是这样的: //pStr是指向以'\\0'结尾的字符串的指针 //steps是要求移动的n void LoopMove ( char * pStr, int steps ) { //请填充... 解答: 正确解答1: void LoopMove ( char *pStr, int steps ) { int n = strlen( pStr ) - steps; char tmp[MAX_LEN]; //这是何物啊??重新定义一个中间数组 strcpy ( tmp, pStr + n );把前n位复制给tmp strcpy ( tmp + steps, pStr);把pstr复制给tmp,从steps位开始 *( tmp + strlen ( pStr ) ) = '\\0';截取strlen()长度, strcpy( pStr, tmp ); 正确解答2: void LoopMove ( char *pStr, int steps ) { int n = strlen( pStr ) - steps; char tmp[MAX_LEN]; memcpy( tmp, pStr + n, steps ); 将后steps位复制给tmp memcpy(pStr + steps, pStr, n ); memcpy(pStr, tmp, steps ); 剖析: 这个试题主要考查面试者对标准库函数的熟练程度,在需要的时候引用库函数可以很大程度上简化程序编写的工作量。 最频繁被使用的库函数包括: (1) strcpy (2) memcpy (3) memset 试题6:已知WAV文件格式如下表,打开一个WAV文件,以适当的数据结构组织WAV文件头并解析WAV格式的各项信息。 WAVE文件格式说明表 解答: 将WAV文件格式定义为结构体WAVEFORMAT: typedef struct tagWaveFormat { char cRiffFlag[4]; UIN32 nFileLen; char cWaveFlag[4]; char cFmtFlag[4]; char cTransition[4]; UIN16 nFormatTag ; UIN16 nChannels; UIN16 nSamplesPerSec; UIN32 nAvgBytesperSec; UIN16 nBlockAlign; UIN16 nBitNumPerSample; char cDataFlag[4]; UIN16 nAudioLength; 假设WAV文件内容读出后存放在指针buffer开始的内存单元内,则分析文件格式的代码很简单,为: WAVEFORMAT waveFormat; 直接通过访问waveFormat的成员,就可以获得特定WAV文件的各项格式信息。 剖析: 试题6考查面试者组织数据结构的能力,有经验的程序设计者将属于一个整体的数据成员组织为一个结构体,利用指针类型转换,可以将memcpy、memset等函数直接用于结构体地址,进行结构体的整体操作。 透过这个题可以看出面试者的程序设计经验是否丰富。 试题7:编写类String的构造函数、析构函数和赋值函数,已知类String的原型为:? 已经封装的文件中写的程序 class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &other); // 拷贝构造函数 ~ String(void); // 析构函数 String & operate =(const String &other); // 赋值函数 private: char *m_data; // 用于保存字符串 解答: 这是在实现文件中编写的程序 //普通构造函数 String::String(const char *str) { if(str==NULL) { m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'\\0'的空 //加分点:对m_data加NULL 判断 *m_data = '\\0'; } else { int length = strlen(str); m_data = new char[length+1]; // 若能加 NULL 判断则更好 strcpy(m_data, str); } } // String的析构函数 String::~String(void) { delete [] m_data; // 或delete m_data; } //拷贝构造函数 String::String(const String &other) // 得分点:输入参数为const型 { int length = strlen(other.m_data); m_data = new char[length+1]; //加分点:对m_data加NULL 判断 strcpy(m_data, other.m_data); } //赋值函数 String & String::operate =(const String &other) // 得分点:输入参数为const型 { if(this == &other) //得分点:检查自赋值 return *this; delete [] m_data; //得分点:释放原有的内存资源 int length = strlen( other.m_data ); m_data = new char[length+1]; //加分点:对m_data加NULL 判断 strcpy( m_data, other.m_data ); return *this; //得分点:返回本对象的引用 剖析: 能够准确无误地编写出String类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经具备了C++基本功的60%以上! 在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。 仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功! const关键字至少有下列n个作用: (1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了; (2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const; (3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值; (4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量; (5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如: operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错: classA a, b, c; (a * b) = c; // 对a*b 操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。、 4.技巧题 试题1:请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回 解答: int checkCPU() { { union w { int a; char b; } c; c.a = 1;0X00 00 00 01四个字节,c.b为一个字节 return (c.b == 1);大端的c.b为00,小端的c.b为01???? }//因为联合体共用一个空间 剖析: 嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。例如,16bit宽的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为: 而在Big-endian模式CPU内存中的存放方式则为: 32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为: 而在Big-endian模式CPU内存中的存放方式则为: 联合体union的存放顺序是所有成员都从低地址开始存放,面试者的解答利用该特性,轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写。如果谁能当场给出这个解答,那简直就是一个天才的程序员。 试题2:写一个函数返回1+2+3+…+n的值(假定结果不会超过长整型变量的范围) 解答: int Sum( int n ) { return ( (long)1 + n) * n / 2; //或return (1l + n) * n / 2; 剖析: 对于这个题,只能说,也许最简单的答案就是最好的答案。下面的解答,或者基于下面的解答思路去优化,不管怎么“折腾”,其效率也不可能与直接return ( 1 l + n ) * n / 2相比! int Sum( int n ) { long sum = 0; for( int i=1; i<=n; i++ ) { sum += i; } return sum; volatile关键字的作用 volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序: short flag; void test() { do1(); while(flag==0); do2(); }这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形式: volatile short flag; 需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。 C语言void及void指针深层探索 1、void的含义 void的字面意思是“无类型”,void *则为“无类型指针”,void *可以指向任何类型的数据。 void几乎只有“注释”和程序的作用,因为从来没有人会定义一个void变量,让我们试着来定义: void a; 这行语句编译时会出错,提示“illegal use of type 'void'”。不过,即使void a的编译不会出错,它也没有任何实际意义。 void真正发挥的作用在于: (1) 对函数返回的限定; (2) 对函数参数的限定。 我们将在第三节对以上二点进行具体说明。 众所周知,如果指针p1和p2的类型相同,那么我们可以直接在p1和p2间互相赋值;如果p1和p2指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。 例如: float *p1; int *p2; p1 = p2; 其中p1 = p2语句会编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为: p1 = (float *)p2; 而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换: void *p1; int *p2; p1 = p2; 但这并不意味着,void *也可以无需强制类型转换地赋给其它类型的指针。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。道理很简单,我们可以说“男人和女人都是人”,但不能说“人是男人”或者“人是女人”。下面的语句编译出错: void *p1; int *p2; p2 = p1; 提示“'=' : cannot convert from 'void *' to 'int *'”。 3.void的使用 下面给出void关键字的使用规则: 规则一 如果函数没有返回值,那么应声明为void类型 在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为void类型。例如: add ( int a, int b ) { return a + b; } int main(int argc, char* argv[]) { printf ( "2 + 3 = %d", add ( 2, 3) ); } 程序运行的结果为输出: 2 + 3 = 5 这说明不加返回值说明的函数的确为int函数。 林锐博士《高质量C/C++编程》中提到:“C++语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。可是编译器并不一定这么认定,譬如在Visual C++6.0中上述add函数的编译无错也无警告且运行正确,所以不能寄希望于编译器会做严格的类型检查。 因此,为了避免混乱,我们在编写C/C++程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为void类型。这既是程序良好可读性的需要,也是编程规范性的要求。另外,加上void类型声明后,也可以发挥代码的“自注释”作用。代码的“自注释”即代码能自己注释自己。 规则二 如果函数无参数,那么应声明其参数为void 在C++语言中声明一个这样的函数: int function(void) { return 1; } 则进行下面的调用是不合法的: function(2); 因为在C++中,函数参数为void的意思是这个函数不接受任何参数。 我们在Turbo C 2.0中编译: #i nclude "stdio.h" fun() { return 1; } main() { printf("%d",fun(2)); getchar(); } 编译正确且输出1,这说明,在C语言中,可以给无参数的函数传送任意类型的参数,但是在C++编译器中编译同样的代码则会出错。在C++中,不能向无参数的函数传送任何参数,出错提示“'fun' : function does not take 1 parameters”。 所以,无论在C还是C++中,若函数不接受任何参数,一定要指明参数为void。 规则三 小心使用void指针类型 按照ANSI(American National Standards Institute)标准,不能对void指针进行算法操作,即下列操作都是不合法的: void * pvoid; pvoid++; //ANSI:错误 pvoid += 1; //ANSI:错误 //ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。 //例如: int *pint; pint++; //ANSI:正确 pint++的结果是使其增大sizeof(int)。 但是大名鼎鼎的GNU(GNU's Not Unix的缩写)则不这么认定,它指定void *的算法操作与char *一致。 因此下列语句在GNU编译器中皆正确: pvoid++; //GNU:正确 pvoid += 1; //GNU:正确 pvoid++的执行结果是其增大了1。 在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码: void * pvoid; (char *)pvoid++; //ANSI:正确;GNU:正确 (char *)pvoid += 1; //ANSI:错误;GNU:正确 GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地迎合ANSI标准。 规则四 如果函数的参数可以是任意类型指针,那么应声明其参数为void * 典型的如内存操作函数memcpy和memset的函数原型分别为: void * memcpy(void *dest, const void *src, size_t len); void * memset ( void * buffer, int c, size_t num ); 这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy和memset的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy和memset明显不是一个“纯粹的,脱离低级趣味的”函数! 下面的代码执行正确: //示例:memset接受任意类型指针 int intarray[100]; memset ( intarray, 0, 100*sizeof(int) ); //将intarray清0 //示例:memcpy接受任意类型指针 int intarray1[100], intarray2[100]; memcpy ( intarray1, intarray2, 100*sizeof(int) ); //将intarray2拷贝给intarray1 有趣的是,memcpy和memset函数返回的也是void *类型,标准库函数的编写者是多么地富有学问啊! 规则五 void不能代表一个真实的变量 下面代码都企图让void代表一个真实的变量,因此都是错误的代码: void a; //错误 function(void a); //错误 void体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人. void的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,也很容易理解void数据类型。正如不能给抽象基类定义一个实例,我们也不能定义一个void(让我们类比的称void为“抽象数据类型”)变量。 关于CONST的用法 const在C语言中算是一个比较新的描述符,我们称之为常量修饰符,意即其所修饰 的对象为常量(immutable)。 我们来分情况看语法上它该如何被使用。 1、函数体内修饰局部变量。 例: void func(){ const int a=0; } 首先,我们先把const这个单词忽略不看,那么a是一个int类型的局部自动变量, 我们给它赋予初始值0。 然后再看const. const作为一个类型限定词,和int有相同的地位。 const int a; int const a; 是等价的。于是此处我们一定要清晰的明白,const修饰的对象是谁,是a,和int没 有关系。const 要求他所修饰的对象为常量,不可被改变,不可被赋值,不可作为 左值(l-value)。 这样的写法也是错误的。 const int a; a=0; 这是一个很常见的使用方式: const double pi=3.14; 在程序的后面如果企图对pi再次赋值或者修改就会出错。 然后看一个稍微复杂的例子。 const int* p; 还是先去掉const 修饰符号。 注意,下面两个是等价的。 int* p; int *p; 其实我们想要说的是,*p是int类型。那么显然,p就是指向int的指针。 同理 const int* p; 其实等价于 const int (*p); int const (*p); 即,*p是常量。也就是说,p指向的数据是常量。 于是 p+=8; //合法 *p=3; //非法,p指向的数据是常量。 那么如何声明一个自身是常量指针呢?方法是让const尽可能的靠近p; int* const p; const右面只有p,显然,它修饰的是p,说明p不可被更改。然后把const去掉,可以 看出p是一个指向 int形式变量的指针。 于是 p+=8; //非法 *p=3; //合法 再看一个更复杂的例子,它是上面二者的综合 const int* const p; 说明p自己是常量,且p指向的变量也是常量。 于是 p+=8; //非法 *p=3; //非法 const 还有一个作用就是用于修饰常量静态字符串。 例如: const char* name="David"; 如果没有const,我们可能会在后面有意无意的写name[4]='x'这样的语句,这样会 导致对只读内存区域的赋值,然后程序会立刻异常终止。有了 const,这个错误就 能在程序被编译的时候就立即检查出来,这就是const的好处。让逻辑错误在编译 期被发现。 const 还可以用来修饰数组 const char s[]="David"; 与上面有类似的作用。 2、在函数声明时修饰参数 来看实际中的一个例子。 NAME memmove -- copy byte string LIBRARY Standard C Library (libc, -lc) SYNOPSIS #i nclude void * memmove(void *dst, const void *src, size_t len); 这是标准库中的一个函数,用于按字节方式复制字符串(内存)。 它的第一个参数,是将字符串复制到哪里去(dest),是目的地,这段内存区域必须 是可写。 它的第二个参数,是要将什么样的字符串复制出去,我们对这段内存区域只做读 取,不写。 于是,我们站在这个函数自己的角度来看,src 这个指针,它所指向的内存内所存 储的数据在整个函数执行的过程中是不变。于是src所指向的内容是常量。于是就 需要用const修饰。 例如,我们这里这样使用它。 const char* s="hello"; char buf[100]; memmove(buf,s,6); //这里其实应该用strcpy或memcpy更好 如果我们反过来写, memmove(s,buf,6); 那么编译器一定会报错。事实是我们经常会把各种函数的参数顺序写反。事实是编 译器在此时帮了我们大忙。如果编译器静悄悄的不报错,(在函数声明处去掉 const即可),那么这个程序在运行的时候一定会崩溃。 这里还要说明的一点是在函数参数声明中const一般用来声明指针而不是变量本身。 例如,上面的size_t len,在函数实现的时候可以完全不用更改len的值,那么是否 应该把len也声明为常量呢?可以,可以这么做。我们来分析这么做有什么优劣。 如果加了const,那么对于这个函数的实现者,可以防止他在实现这个函数的时候修 改不需要修改的值(len),这样很好。 但是对于这个函数的使用者, 1。这个修饰符号毫无意义,我们可以传递一个常量整数或者一个非常量整数过 去,反正对方获得的只是我们传递的一个copy。 2。暴露了实现。我不需要知道你在实现这个函数的时候是否修改过len的值。 所以,const一般只用来修饰指针。 再看一个复杂的例子 int execv(const char *path, char *const argv[]); 着重看后面这个,argv.它代表什么。 如果去掉const,我们可以看出 char * argv[]; argv是一个数组,它的每个元素都是char *类型的指针。 如果加上const.那么const修饰的是谁呢?他修饰的是一个数组,argv[],意思就是 说这个数组的元素是只读的。那么数组的元素的是什么类型呢?是char *类型的指 针.也就是说指针是常量,而它指向的数据不是。 于是 argv[1]=NULL; //非法 argv[0][0]='a'; //合法 3、全局变量。 我们的原则依然是,尽可能少的使用全局变量。 我们的第二条规则 则是,尽可能多的使用const。 如果一个全局变量只在本文件中使用,那么用法和前面所说的函数局部变量没有什 么区别。 如果它要在多个文件间共享,那么就牵扯到一个存储类型的问题。 有两种方式。 1.使用extern 例如 /* file1.h */ extern const double pi; /* file1.c */ const double pi=3.14; 然后其他需要使用pi这个变量的,包含file1.h #i nclude "file1.h" 或者,自己把那句声明复制一遍就好。 这样做的结果是,整个程序链接完后,所有需要使用pi这个变量的共享一个存储区域。 2.使用static,静态外部存储类 /* constant.h */ static const pi=3.14; 需要使用这个变量的*.c文件中,必须包含这个头文件。 前面的static一定不能少。否则链接的时候会报告说该变量被多次定义。 这样做的结果是,每个包含了constant.h的*.c文件,都有一份该变量自己的copy, 该变量实际上还是被定义了多次,占用了多个存储空间,不过在加了static关键字 后,解决了文件间重定义的冲突。 坏处是浪费了存储空间,导致链接完后的可执行文件变大。但是通常,这个,小小 几字节的变化,不是问题。 好处是,你不用关心这个变量是在哪个文件中被初始化的。 最后,说说const的作用。 const 的好处,是引入了常量的概念,让我们不要去修改不该修改的内存。直接的 作用就是让更多的逻辑错误在编译期被发现。所以我们要尽可能的多使用const。 但是很多人并不习惯使用它,更有甚者,是在整个程序 编写/调试 完后才补 const。如果是给函数的声明补const,尚好。如果是给 全局/局部变量补const,那 么……那么,为时已晚,无非是让代码看起来更漂亮了。 c语言中的结构(struct)和联合(union 联 合(union) 1. 联合说明和联合变量定义 联合也是一种新的数据类型, 它是一种特殊形式的变量。 联合说明和联合变量定义与结构十分相似。其形式为: union 联合名{ 数据类型成员名; 数据类型成员名; ... } 联合变量名; 联合表示几个变量公用一个内存位置, 在不同的时间保存不同的数据类型 和不同长度的变量。 下例表示说明一个联合a_bc: union a_bc{ int i; char mm; }; 再用已说明的联合可定义联合变量。 例如用上面说明的联合定义一个名为lgc的联合变量, 可写成: union a_bc lgc; 在联合变量lgc中, 整型量i和字符mm公用同一内存位置。 当一个联合被说明时, 编译程序自动地产生一个变量, 其长度为联合中最大的变量长度。 联合访问其成员的方法与结构相同。同样联合变量也可以定义成数组或指针,但定义为指针时, 也要用"->;"符号, 此时联合访问成员可表示成: 联合名->;成员名 另外, 联合既可以出现在结构内, 它的成员也可以是结构。 例如: struct{ int age; char *addr; union{ int i; char *ch; }x; }y[10]; 若要访问结构变量y[1]中联合x的成员i, 可以写成: y[1].x.i; 若要访问结构变量y[2]中联合x的字符串指针ch的第一个字符可写成: *y[2].x.ch; 若写成"y[2].x.*ch;"是错误的。 2. 结构和联合的区别 结构和联合有下列区别: 1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合转只存放了一个被选中的成员, 而结构的所有成员都存在。 2. 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。 下面举一个例了来加对深联合的理解。 例4: main() { union{ /*定义一个联合*/ int i; struct{ /*在联合中定义一个结构*/ char first; char second; }half; }number; number.i=0x4241; /*联合成员赋值*/ printf("%c%c\\n", number.half.first, mumber.half.second); number.half.first='a'; /*联合中结构成员赋值*/ number.half.second='b'; printf("%x\\n", number.i); getch(); } 输出结果为: AB 6261 从上例结果可以看出: 当给i赋值后, 其低八位也就是first和second的值;当给first和second赋字符后, 这两个字符的ASCII码也将作为i 的低八位和高八位。 Volatile 关键字告诉编译器不要持有变量的临时性拷贝。一般用在多线程程序中,以避免在其中一个线程操作该变量时,将其拷贝入寄存器。请看以下情形: A线程将变量复制入寄存器,然后进入循环,反复检测寄存器的值是否满足一定条件(它期待B线程改变变量的值。 在此种情况下,当B线程改变了变量的值时,已改变的值对其在寄存器的值没有影响。所以A线程进入死循环。 volatile 就是在此种情况下使用。 堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。 3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区—存放函数体的二进制代码。 例子程序 这是一个前辈写的,非常详细 //main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main() { int b; 栈 char s[] = "abc"; 栈 char *p2; 栈 char *p3 = "123456"; 123456\\0在常量区,p3在栈上。 static int c =0; 全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); 分配得来得10和20字节的区域就在堆区。 strcpy(p1, "123456"); 123456\\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 } 二、堆和栈的理论知识 2.1申请方式 stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 heap: 需要程序员自己申请,并指明大小,在c中malloc函数 如p1 = (char *)malloc(10); 在C++中用new运算符 如p2 = (char *)malloc(10); 但是注意p1、p2本身是在栈中的。 2.2申请后系统的响应 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 2.4申请效率的比较 栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 2.5堆和栈中的存储内容 栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 2.6存取效率的比较 char s1[] = "aaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb"; aaaaaaaaaaa是在运行时刻赋值的; 而bbbbbbbbbbb是在编译时就确定的; 但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 比如: #i nclude void main() { char a = 1; char c[] = "12345670"; char *p ="12345670"; a = c[1]; a = p[1]; return; } 对应的汇编代码 10: a = c[1]; 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 0040106A 88 4D FC mov byte ptr [ebp-4],cl 11: a = p[1]; 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 00401070 8A 42 01 mov al,byte ptr [edx+1] 00401073 88 45 FC mov byte ptr [ebp-4],al 第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 2.7小结 堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 windows进程中的内存结构 在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。 首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码: #i nclude int g1=0, g2=0, g3=0; int main() { static int s1=0, s2=0, s3=0; int v1=0, v2=0, v3=0; //打印出各个变量的内存地址 printf("0x%08x\\n",&v1); //打印各本地变量的内存地址 printf("0x%08x\\n",&v2); printf("0x%08x\\n\\n",&v3); printf("0x%08x\\n",&g1); //打印各全局变量的内存地址 printf("0x%08x\\n",&g2); printf("0x%08x\\n\\n",&g3); printf("0x%08x\\n",&s1); //打印各静态变量的内存地址 printf("0x%08x\\n",&s2); printf("0x%08x\\n\\n",&s3); return 0; } 编译后的执行结果是: 0x0012ff78 0x0012ff7c 0x0012ff80 0x004068d0 0x004068d4 0x004068d8 0x004068dc 0x004068e0 0x004068e4 输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈 (stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。 ├———————┤低端内存区域 │ …… │ ├———————┤ │ 动态数据区 │ ├———————┤ │ …… │ ├———————┤ │ 代码区 │ ├———————┤ │ 静态数据区 │ ├———————┤ │ …… │ ├———————┤高端内存区域 堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码: #i nclude void __stdcall func(int param1,int param2,int param3) { int var1=param1; int var2=param2; int var3=param3; printf("0x%08x\\n",?m1); //打印出各个变量的内存地址 printf("0x%08x\\n",?m2); printf("0x%08x\\n\\n",?m3); printf("0x%08x\\n",&var1); printf("0x%08x\\n",&var2); printf("0x%08x\\n\\n",&var3); return; } int main() { func(1,2,3); return 0; } 编译后的执行结果是: 0x0012ff78 0x0012ff7c 0x0012ff80 0x0012ff68 0x0012ff6c 0x0012ff70 ├———————┤<—函数执行时的栈顶(ESP)、低端内存区域 │ …… │ ├———————┤ │ var 1 │ ├———————┤ │ var 2 │ ├———————┤ │ var 3 │ ├———————┤ │ RET │ ├———————┤<—“__cdecl”函数返回后的栈顶(ESP) │ parameter 1 │ ├———————┤ │ parameter 2 │ ├———————┤ │ parameter 3 │ ├———————┤<—“__stdcall”函数返回后的栈顶(ESP) │ …… │ ├———————┤<—栈底(基地址 EBP)、高端内存区域 上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码: ;--------------func 函数的汇编代码------------------- :00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间 :00401003 8B442410 mov eax, dword ptr [esp+10] :00401007 8B4C2414 mov ecx, dword ptr [esp+14] :0040100B 8B542418 mov edx, dword ptr [esp+18] :0040100F 442400 mov dword ptr [esp], eax :00401013 8D442410 lea eax, dword ptr [esp+10] :00401017 4C2404 mov dword ptr [esp+04], ecx ……………………(省略若干代码) :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间 :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间 ;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复 ;-------------------函数结束------------------------- ;--------------主程序调用func函数的代码-------------- :00401080 6A03 push 00000003 //压入参数param3 :00401082 6A02 push 00000002 //压入参数param2 :00401084 6A01 push 00000001 //压入参数param1 :00401086 E875FFFFFF call 00401000 //调用func函数 ;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C” 聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码: #i nclude #i nclude void __stdcall func() { char lpBuff[8]="\\0"; strcat(lpBuff,"AAAAAAAAAAA"); return; } int main() { func(); return 0; } 编译后执行一下回怎么样?哈,“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”,“非法操作”喽! "41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的\\0,那 strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个\\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。 ├———————┤<—低端内存区域 │ …… │ ├———————┤<—由exploit填入数据的开始 │ │ │ buffer │<—填入无用的数据 │ │ ├———————┤ │ RET │<—指向shellcode,或NOP指令的范围 ├———————┤ │ NOP │ │ …… │<—填入的NOP指令,是RET可指向的范围 │ NOP │ ├———————┤ │ │ │ shellcode │ │ │ ├———————┤<—由exploit填入数据的结束 │ …… │ ├———————┤<—高端内存区域 windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码: #i nclude #i nclude #i nclude void func() { char *buffer=new char[128]; char bufflocal[128]; static char buffstatic[128]; printf("0x%08x\\n",buffer); //打印堆中变量的内存地址 printf("0x%08x\\n",bufflocal); //打印本地变量的内存地址 printf("0x%08x\\n",buffstatic); //打印静态变量的内存地址 } void main() { func(); return; } 程序执行结果为: 0x004107d0 0x0012ff04 0x004068c0 可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数: HeapAlloc 在堆中申请内存空间 HeapCreate 创建一个新的堆对象 HeapDestroy 销毁一个堆对象 HeapFree 释放申请的内存 HeapWalk 枚举堆对象的所有内存块 GetProcessHeap 取得进程的默认堆对象 GetProcessHeaps 取得进程所有的堆对象 LocalAlloc GlobalAlloc 当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间: HANDLE hHeap=GetProcessHeap(); char *buff=HeapAlloc(hHeap,0,8); 其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧: #pragma comment(linker,"/entry:main") //定义程序的入口 #i nclude _CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf /*--------------------------------------------------------------------------- 写到这里,我们顺便来复习一下前面所讲的知识: (*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。 由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是 __stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。 ---------------------------------------------------------------------------*/ void main() { HANDLE hHeap=GetProcessHeap(); char *buff=HeapAlloc(hHeap,0,0x10); char *buff2=HeapAlloc(hHeap,0,0x10); HMODULE hMsvcrt=LoadLibrary("msvcrt.dll"); printf=(void *)GetProcAddress(hMsvcrt,"printf"); printf("0x%08x\\n",hHeap); printf("0x%08x\\n",buff); printf("0x%08x\\n\\n",buff2); } 执行结果为: 0x00130000 0x00133100 0x00133118 hHeap 的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。 最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果: #i nclude int main() { int a; char b; int c; printf("0x%08x\\n",&a); printf("0x%08x\\n",&b); printf("0x%08x\\n",&c); return 0; } 这是用VC编译后的执行结果: 0x0012ff7c 0x0012ff7b 0x0012ff80 变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。 这是用Dev-C++编译后的执行结果: 0x0022ff7c 0x0022ff7b 0x0022ff74 变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。 这是用lcc编译后的执行结果: 0x0012ff6c 0x0012ff6b 0x0012ff 变量在内存中的顺序:同上。 三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。 基础知识: 堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从 EIP寄存器中读取下一条指令的内存地址,然后继续执行。 堆(Heap)栈(Stack) 1、内存分配方面: 堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式是类似于链表。可能用到的关键字如下:new、malloc、delete、free等等。 栈:由编译器(Compiler)自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、申请方式方面: 堆:需要程序员自己申请,并指明大小。在c中malloc函数如p1 = (char *)malloc(10);在C++中用new运算符,但是注意p1、p2本身是在栈中的。因为他们还是可以认为是局部变量。 栈:由系统自动分配。 例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。 3、系统响应方面: 堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 4、大小方面: 堆:是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 栈:在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 5、效率方面: 堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 栈:由系统自动分配,速度较快。但程序员是无法控制的。 6、存放内容方面: 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 栈:在函数调用时第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈,然后是函数中的局部变量。 注意: 静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 7、存取效率方面: 堆:char *s1 = "Hellow Word";是在编译时就确定的; 栈:char s1[] = "Hellow Word"; 是在运行时赋值的;用数组比用指针速度要快一些,因为指针在底层汇编中需要用edx 10月21号moto的笔试是在我们学校举行的,120分钟的笔试时间,试题内容大致涵盖了c/c++,数据库,数字通信,IQ等方面。我自我感觉做的很顺利,至少两道编程题我都搞定了。一个多小时就把试题做完了,于是,我就把一些试题偷偷抄到了草稿纸上,回去和大家一齐分享。下面就是我抄回来的试题: 1。打印如下图案,共19行,只能有一个for循环(程序已经提供) * *** ***** ******* ********* *********** ************* *************** ***************** ******************* ***************** *************** ************* *********** ********* ******* ***** *** * for(i=0;i<19;i++) { } 在网上搜寻了些解答,如下: 1. #include #include #include using namespace std; int main() { int a[2]={-1,1}; string str="*******************"; int i=0,j=9,t=1; for(i=0;i<19;++i) { j+=a[i<10]; if(i!=0)t=t+a[i<10]*2; cout< } return 0; } 2. #include #include int main() { char * tempStr = "*******************"; char * blank = " "; int i = 0; char * dataStr [20]; char blankstr[10]; for ( i=0; i<19; i++) { memset(dataStr, 0, 20); memset(blankstr,0,10); if (i<10) { memcpy(blankstr,blank,9-i); memcpy(dataStr, tempStr, (i+1)*2-1); } else { memcpy(blankstr,blank,i-9); memcpy(dataStr, tempStr, (19-i)*2-1); } printf("%s",blankstr); printf("%s\\n", dataStr); } return 0; } 3。 #include #include #include #define MAXSIZE 19 void main() { int i,k; char ch; char format[30],stars[30],tmp[3]; ch = '*'; strcpy(stars,""); for(i=0; i { if(i <= MAXSIZE/2) { k = (MAXSIZE + 1)/2 - i; strcpy(format,"%"); itoa(k,tmp,10); strcat(format,tmp); strcat(format,"c"); if(i>0) { strcat(stars,"**"); strcat(format,stars); } printf(format,ch); printf("\\n"); } else { k = i + 2 - (MAXSIZE + 1)/2; strcpy(format,"%"); itoa(k,tmp,10); strcat(format,tmp); strcat(format,"c"); stars[MAXSIZE -1 -(2*(i+1) - MAXSIZE -1)] = 0; strcat(format,stars); printf(format,ch); printf("\\n"); } } //getchar(); //getchar(); } 注:printf格式%mc表示输入字符宽度为m位,左边补空格. 4。 有高人发的回帖: #include int main(int argv,int * argc[]) { for(int i=0;i<19;i++) { static char cc[19] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}; //19个空格 static const int j = 19 / 2; //j值为9 static int count = 0; //count为离中间元素的距离 if( i < ( 19 / 2 + 1)) { cc[j - count] = '*'; cc[j + count] = '*'; cout << cc << endl; count++; } else { count--; cc[j - count] = ' '; cc[j + count] = ' '; cout << cc << endl; } } return 0; } 一看就知差距,我的晦涩难懂 我想用两个数组,一个存放空格数量,一个存放'*' 但是下面这个程序只用了一个数组,把每一行输出的形状存到数组中,妙!我咋就没想到?! 2. void fun(int x) { if(x/2>0)fun(x/2); printf("%d",x); } 求fun(10)的输出结果 //12510,一个递归,很简单的 3。 #define f1(n) (n)*(n) int i=5; int k=0; k=f1(i++); printf("%d %d",i,k); 输出结果: :// 7 25,++放后面是后算的 4。下面那个for循环是无限循环? for(int i=010;i==10;i+=0) //这个明显不是,010等于8,8 != 10,一判断就出去了 for(int i=10;(i++^--i)==0;i+=0) //无限循环 还有几个忘了 5.Email relay 和Email access分别用了什么协议?(其他还有很多,略) SMTP,POP3 注: SMTP:Simple MAIL Transfer Protocol,简单邮件传输协议,互联网上的电子邮件发送就是使用这个协议。SMTP open relay host:开放的SMTP中继主机——它很可能被恶意的人利用,来产生大量垃圾邮件. POP3协议允许客户机通过(临时的或永久的)TCP/IP连接或其他网络协议的连接,从一个服务器(这时就称为POP3服务器)上获取电子邮件(E-MAIL),POP3不负责发送邮件. 6。in a mobile phone,软件运行期,stack data 存在于 a.rom,b.flash c.eeprom d.ram e.none of the above 答:D.ram。这题稍微涉及到一点硬件知识,ROM的全称是Read Only Memory,即只读存储器,flash ,eeprom都是ROM家族的一员,RAM是Random Access Memory的简称,意为随机存取存储器,也就是内存了。不管是堆还是栈都是放在内存里的。 8. char a[2][2][3]={{{1,6,3},{5,4,15}},{{3,5,33},{23,12,7}} }; for(int i=0;i<12;i++) printf("%d ",_______); //a[i/6][((i>=6 ? (i-6):i)+2)/5][i%3]); 或者:*(*(*(a+i/6)+(i/3%2))+i%3) *((char *)a + i) //*(**a + i) 空格处填上合适的语句,顺序打印出a中的数字??????????? 9。void fun(char *a,char *b) { a=b; (*a)++;//把b的地址给a,访问a的地址相当于访问b的地址也就是访问s2,于是本函数只是该变了s2的值,s1没被改变 } void main() { char s1='A',s2='b'; char *p1=&s1; char *p2=&s2; fun(p1,p2); printf("%c%c",s1,s2); 输出结果: // Ac吧,这个比较简单考完出来,我是很有信心能进入面试的,等了几天,陆续有人收到通知,我却没有。我的信心大受打击,怎么可能呢!?后来了解到,moto的笔试25道选择题做对19道才能参加面试,后面的题目基本不看。回想下,我后面填空和大题做的不错可是选择题确实做的不咋的( 郁闷啊,给宿舍的同学鄙视了:你的水平也不行啊~~~) 这个时候我开始感觉有点压力了,有点危机感了 MTK深圳公司嵌入式软件工程师笔试题 //MTK深圳嵌入式软件工程师笔试题(B卷) //1***************************************** #define pi 3.14 #define Area(R) pi*R*R main() { int r1=5,r2=2; double s=0; s=Area(r1-r2); printf("The area is %f",s); } //求结果 我:3.14*5-2*5-2= //2********************************************* //函数 int compare(int a,int b),定义为该函数的函数指针P:为___ 我:int(*p)(int,int)__总结:将函数名换为*p即可__________ //3********************************************* #include void sub(char*s,int num) { int i ,j=num; char t; while(j-->1) { for(i=0;i { if(s[i] { t=s[i]; s[i]=s[i+1]; s[i+1]=t; } } } } main() { char*s="CEAeded"; sub(s,6); printf("%s\\n",s) } //求结果 我eeddECA //4********************************************** //交换两个变量的值,不使用第三个变量,即a=3,b=5交换 //后b=3,a=5 unsigned char a=3,b=5; //5************************************************** #define N 100 void GetMemory1(char*p) { p=(char*)malloc(sizeof(char)*N); strcpy(p,"Have a good day!"); } char*GetMemory2(void) { char p[]="Have a good day!"; return p; } void main(void) { char*str1=NULL,*str2=NULL; GetMemory1(str1); GetMemory2(str2); printf("\\nstr1:%s",str1); printf("\\nstr2:%s",str2);
代码 代码: /* File name: hello.h
代码: /* File name: hello.c : /* File name: main.cxx : PROGRAM := hello # 设置运行程序名 } } } } } } } } } } } } GetMemory( str ); return p; *p = (char *) malloc( num ); } char *str = (char *) malloc(100); str = NULL; } } sizeof ( p ) = ? sizeof ( p ) = 4 cout << sizeof(str) << endl; 注意与上面的数组作函数形参区别,讲解如下: least = MIN(*p++, b); #define MIN(A,B) ((A) <= (B) ? (A) : (B)) #define MIN(A,B) (A <= B ? A : B ) #endif /* __INCvxWorksh */ #endif void foo(int x, int y); } } } 偏移地址 字节数 数据类型 内 容 文件头 00H 4 Char "RIFF"标志 04H 4 int32 文件长度 08H 4 Char "WAVE"标志 0CH 4 Char "fmt"标志 10H 4 过渡字节(不定) 14H 2 int16 格式类别 16H 2 int16 通道数 18H 2 int16 采样率(每秒样本数),表示每个通道的播放速度 1CH 4 int32 波形音频数据传送速率 20H 2 int16 数据块的调整数(按字节算的) 22H 2 每样本的数据位数 24H 4 Char 数据标记符"data" 28H 4 int32 语音数据的长度 } WAVEFORMAT; memcpy( &waveFormat, buffer,sizeof( WAVEFORMAT ) ); }; } const classA operator*(const classA& a1,const classA& a2); 的结果赋值 } 内存地址 存放内容 0x4000 0x34 0x4001 0x12 内存地址 存放内容 0x4000 0x12 0x4001 0x34 内存地址 存放内容 0x4000 0x78 0x4001 0x56 0x4002 0x34 0x4003 0x12 内存地址 存放内容 0x4000 0x12 0x4001 0x34 0x4002 0x56 0x4003 0x78 } } )简介
suningin详解笔试面试题(6)--moto笔试寄存器中转一下,而数组在栈上直接读取。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- fenyunshixun.cn 版权所有 湘ICP备2023022495号-9
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务