[C/C++笔试面试题] 程序设计基础 - 位操作、函数、数组篇

7 位操作

二进制是现代计算机发展的基础,所有的程序代码都需要转换成最终的二进制代码才能执 行。合理地进行二进制的位操作,对于编写优质代码,特别是嵌入式应用软件开发非常关键。

7.1 一些结构声明中的冒号和数字是什么意思?

c语言的结构体可以实现位段,它的定义形式是在一个定义的结构体成员后面加上冒号, 然后是该成员所占的位数。位段的结构体成员必须是int或者unsigned int类型,不能是其他类型。位段在内存中的存储方式是由具体的编译器决定的。

示例程序如下:

#include <stdio.h> 

typedef struct
{
    int a:2; 
    int b:2; 
    int c:l;
}test;

int main()
{
    test t; 
    
    t.a = 1; 
    t.b = 3; 
    t.c = 1;
    printf("%d\n%d\n%d\n%d\n",t.a, t.b, t.c, sizeof(test)); 
    
    return 0;
}

输出结果如下:

1
-1
-1
4

由于a占两位,而a被赋值为1,二进制就是01,因此%d输出的时候输出1; b也占了两 位,赋值为3,二进制也就是11,由于使用了%d输出,表示的是将这个b作为有符号int型来输出,这样的话二进制的11将会有一位被认为是符号位,并且两位的b也会被扩展为int类 型,也就是4字节,即32位。

其实a也做了这种扩展,只是扩展符号位的时候,由于数字在计算机中存储都是补码形式,因此扩展符号位的时候正数用0填充高位,负数则用1填充高位。因此对于a来说,输出的时候被扩展为00000000 00000000 00000000 00000001,也就是 1,而b则扩展为11111111 11111111 11111111 11111111,也就是-1了,c的显示也是这样的。


7.2 如何快速求取一个整数的7倍?

可以先将此整数左移3位(相当于将数字 乘以8),然后再减去原值,即(x<<3)-x就获得了x的7倍。此处需要注意的是,由于-的优先级高于<<,所以不能去掉括号,否则结果不正确。


7.3 如何实现位操作求两个数的平均值?

一般而言,求解平均数的方法就是将两者相加,然后除以2,以变量x与y为例,两者的平均数为 (x+y)/2。

但是采用上述方法,会存在一个问题,当两个数比较大时,如两者的和大于了机器位数能够表示的最大值,可能会存在数据溢出的情况,而采用位运算方法则可以避免这一问题, (x&y)+((x^y)>>1)方式表达的意思都是求解变量x与y的平均数,而且位运算相比除法运算, 效率更高。

扫描二维码关注公众号,回复: 4711077 查看本文章

示例程序如下:

#include <stdio.h>

int main()
{
    int x = 2147483647, y = 2147483647;
    
    printf("%d\n",(x+y)/2); 
    printf("%d\n",(x&y)+((x^y)>>1)); 
    
    return 0;
}

在32位机器下,程序输出结果如下:

-1

2147483647

程序的输出正好验证了这一算法的可行性。


引申:如何利用位运算计算数的绝对值?

以X为负数为例来分析。因为在计算机中,数字都是以补码的形式存放的,求负数的绝对值,应该是不管符号位,执行按位取反,末位加1操作即可。

对于一个负数,将其右移31位后会变成0xffffffff,而对于一个正数而言,右移31位则为 0x00000000,而0xffffffff^x + x = -1,因为 1011 ^ 1111 =0100 ,任何数与 1111 异或,其实质都是把x的0和1进行颠倒计算。如果用变量y表示x右移31位,(x^y)-y则表示的是x的绝对值。

程序示例如下:

#include<stdio.h> 

int MyAbs( int x)
{
    int y;
    y = x >> 31 ; //若x为正数,则y为0;若x为负数,则x为-1
    return (x^y)-y ;//此处还可以写为(x+y)^y
}

int main()
{
    printf("%d\n", MyAbs(2)); 
    printf("%d\n", MyAbs(-2));
    
    return 0;
}

程序输出结果:

2
2

上例中,在函数MyAbs中,对局部变量y进行赋值时,由于是对x进行右移31位,如果 x为正数,则y=0;如果x为负数,则y = -1。


7.4 unsigned int i=3; printf("%u\n", i*-1)输出为多少?

示例程序如下:

#include <stdio.h> 

int main()
{
    unsigned int i=3; 
    printf("%u\n",i*-l); 
    
    return 0;
}

程序输出结果:

4294967293

在32位机器中,i*-l的值为4294967293。在32位机器中,无符号int的值域是[0,4294967295],有符号int的话,值域是[-2147483648,2147483647],两个值域的个数都是4294967296个,即

[0,2147483647]U[2147483648,4294967295] = [0,4294967295]

有符号int的[-2147483648,-1]对应于无符号int的[2147483648,4294967295]区域,两个区域的值是一一映射关系。所以,-1对应4294967295,-2对应4294967294, -3对应4294967293。


7.5 如何求解整型数的二进制表示中1的个数?

求解整型数的二进制表示中1的个数有以下两种方法: 方法一,程序代码如下:

#include <stdio.h> 

int func(int x)
{
    int countx = 0; 
    while(x)
    {

        countx++; 
        x = x&(x-1);
    }

    return countx;
}

int main()
{
    printf("%d\n", func(9999)); 
    
    return 0;
}

程序输出结果:

8

在上例中,函数func()的功能是将x转化为二进制数,然后计算该二进制数中含有的1 的个数。首先以9为例来分析,9的二进制为1001,8的二进制为1000,两者执行&操作之后 结果为1000,此时1000再与0111 (7的二进制位)执行&操作之后结果为0。

为了理解这个算法的核心,需要理解以下两个操作:

(1) 当一个数被减1时,它最右边的那个值为1的bit将变为0,同时其右边的所有的bit 都会变成1。

(2) “&=”,位与并赋值操作。去掉已经被计数过的1,并将该值重新设置给n。这个算法循环的次数是bit位为1的个数。也就说,有几个bit为1,循环几次,对bit为1比较稀疏的数来说,性能很好。例如,0x1000 0000循环一次就可以。

方法二,判断每个数的二进制表示中每一位是否为1,如果为1,就在count上加1,而循环的次数是常数,即n的位数。但该方法有一个缺陷,就是在1比较稀疏的时候效率会比较低。
程序示例如下:

#include <stdio.h> 

int func (int n)
{
    int count=0; 
    
    while (n)
    {
        count += n & Ox1u ;
        n >>= 1 ;
    }
    
    return count;
}

int main()
{
    printf("%d\n",func(9999)); 
    
    return 0;
}

程序输出结果:

8


7.6 不能用sizeof()函数,如何判断操作系统是16位还是32位的?

如果没有强调不许使用sizeof, —般可以使用sizeof计算字节长度来判断操作系统的位 数,如在32位机器上,sizeof(int) = 4,而在16位机器上,sizeof(int)=2。除此之外,还有以下两种方法。

方法一:一般而言,机器位数不同,其表示的数字的最大值也不同,根据这一特性,可以 判断操作系统的位数。

例如,运行如下代码:

#include <stdio.h> 

int main()
{
    int i = 65536; 
    printf("%d\n",i); 
    int j = 65535; 
    printf("%d\n",j); 
    
    return 0;
}

由于16位机器下,无法表示这么大的数,会出现越界情况,所以程序输出为

0
1

而在32位机器下,则会正常输出,程序输出为 65536 65535。之所以会有区别,是因为在16位机器下,能够表示的最大数为65535,所以会存在最高位溢出的情况。当变量的值为65536时,输出为0;当变量的值为65535时,输出为-1。而在 32位机器上,则不会出现溢出的情况,所以输出为正常输出。

方法二:对0值取反,不同位数下的0值取反,其结果不一样。例如,在32位机器下, 按位取反运算,结果为11111111111111111111111111111111。运行如下代码:

#include <stdio.h> 

int main()
{
    unsigned int a =〜0; 
    
    if( a>65536)
        printf("32 位\11");
    else
        printf(”16 位\11"); 
        
    return 0;
}

程序输出为:

32位


7.7 嵌人式编程中,什么是大端?什么是小端?

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。如下图所示:

引申:如何判断计算机处理器是大端还是小端?

方法一:可以通过指针地址来判断,由于在32位计算机系统中,int占4个字节,char占1个 字节,所以可以采用如下做法实现该判断。示例程序如下:

#include <iostream>
#include <stdio.h>

using namespace std;

int fun()
{
    int num = 0x12345678;

    // (char*)&num获得num的起始地址,*((char*)&num)是num的起始地址所指向的值。
    return (*((char*)&num) == 0x12 )?1:0; // 本机返回1,为大端 返回0,为小端
}

int main()
{
    cout << fun() << endl;
}

方法二:联合体union的存放顺序是所有成员都从低地址开始存放。

#include <iostream>
#include <stdio.h>

using namespace std;

//判断系统是大端还是小端:通过联合体,因为联合体的所有成员都从低地址开始存放
int fun()
{
    union test
    {
        int i;
        char c;
    };

    test t;
    t.i = 1;

    //如果是大端,则t.c为0x00,则t.c!=1,返回0 是小端,则t.c为0x01,则t.c==1,返回1
    return (t.c==1);
}

int main()
{
    cout << fun() << endl;
}


7.8 怎么样写一个接受可变参数的函数?

C语言中支持函数调用的参数为变参形式。例如,printf()这个函数,它的函数原型是int printf( const char* format, ...),它除了有一个参数format固定以外,后面跟的参数的个数和类型都是可变的,可以有以下多种不同的调用方法:

(1) printf("%d",i);

(2) printf("%s",s);

(3) printf("the number is %d ,string is:%s", i, s);

printf()函数是一个有着变参的库函数,在C语言中,程序员也可以根据实际的需求编写 变参函数。如下程序示例代码,实现了一个变参函数add2(),该函数实现多参数求和运算。

#include <stdio.h>

//num使用int类型也可以,这里只是为了节省内存
int add2(char num,...)
{
    int sum = 0;
    int index = 0;

    int *p = (int *)&num+1;
    for (; index<(int)num; index++)
    {
        sum += *p++;
    }

    return sum;
}

int main()
{
    int i = 1;
    int j = 2;
    int k = 3;

    printf("%d\n",add2(3,i,j,k));

    return 0;    
}

程序输出结果:

6


8 函数

函数是程序的基本组成单位,利用函数,不仅能够实现程序的模块化,而且简单、直观, 能极大地提高程序的易读性和可维护性,所以将程序中的一些计算或操作抽象成函数以供随时调用,理解函数的执行原理以及应用是一个优秀程序员应该具备的基本能力。

8.1 函数模板/模板函数、类模板/模板类?

(1) 函数模板/模板函数。

函数模板是对一批模样相同的函数的说明描述,它不是某一个具体的函数;而模板函数则是将函数模板内的“数据类型参数”具体化后得到的重载函数(就是由模板而来的函数)。简单地说,函数模板是抽象的,而模板函数则是具体的。

函数模板减少了程序员输入代码的工作量,是C++中功能最强的特性之一,是提高软件代码重用性的重要手段之一。函数模板的形式一般如下所示:

 template <模板类型形参表>

<返回值类型> <函数名>(模板函数形参表)
{
    //函数体
}

其中 <模板函数形参表> 的类型可以是任何类型,包括基本数据类型和类类型。需要注意的是,函数模板并不是一个实实在在的函数,它是一组函数的描述,它并不能直接执行,需要实例化为模板函数后才能执行,而一旦数据类型形参实例化以后,就会产生一个实实在在的模板函数了。

(2) 类模板/模板类。

类模板与函数模板类似,将数据类型定义为参数,描述了代码类似的部分类的集合,具体化为模板类后,可以用于生成具体的对象。

template <类型参数表> 

class<类名>
{
    //类说明体
}; 

template <类型形参表>
<返回类型> <类名>< 类型名表 >::<成员函数1>(形参表)
{
    //成员函数定义体
}

其中 <类型形参表> 与函数模板中的 <类型形参表> 意义类似,而类模板本身不是一个真实的类,只是对类的一种描述,必须用类型参数将其实例化为模板类后,才能用来生成具体的对 象。简而言之,类是对象的抽象,而类模板就是类的抽象。

具体而言,C++中引入模板类主要有以下5个方面的好处:

1) 可用来创建动态增长和减小的数据结构。

2) 它是类型无关的,因此具有很高的可复用性。

3) 它在编译时而不是运行时检查数据类型,保证了类型安全。

4) 它是平台无关的,可移植性强。

5) 可用于基本数据类型。


8.2 C++函数传递参数的方式有哪些?

当进行函数调用时,要填入与函数形式参数个数相同的实际参数,在程序运行过程中,实际参数(简称实参)就会将参数值传递给相应的形式参数(简称形参),然后在函数中实现对数据的处理和返回。C++函数传递参数的方式一般有以下4种:

(1) 值传递

当进行值传递时,就是将实参的值复制到形参中,而形参和实参不是同一个存储单元,所以函数调用结束后,实参的值不会发生改变。程序示例如下:

#include <iostream> 
using namespace std; 

void swap(int a,int b)
{
    int temp; 
    temp=a;
    a=b;
    b=temp;
    cout<<a<<","<<b<<endl;
}

int main()
{
    int x=l; 
    int y=2; 
    swap(x,y);
    cout<<x<<", "<<y<<endl; 
    
    return 0;
}
程序的输出结果:
2,1
1,2

也就是说,在进行函数调用的时候只交换了形参的值,而并未交换实参的值,形参值的改变并没有改变实参的值。

(2) 指针传递

当进行指针传递时,形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)指向实参变量单元。这种方式还是“值传递”,只不过实参的值是变量的地址而已。而在函数中改变的不是实参的值,而是实参地址所指向的变量的值。

程序示例如下:

#include <iostream> 
using namespace std; 

void swap(int *a,int *b)
{
    int temp; 
    temp=*a;
    *a=*b;
    *b=temp;
    cout<<*a<<","«*b«endl;
}

int main()
{
    int x=l; 
    int y=2; 
    
    swap(&x,y);     
    cout<<x<<","<<y<<endl; 
    
    return 0;
}
程序的输出结果:
2,1
2,1

也就是说,在进行函数调用时,不仅交换了形参的值,而且交换了实参的值。

(3) 引用传递

实参地址传递到形参,使形参的地址取实参的地址,从而使形参与实参共享同一单元的方式。

程序代码示例如下:

#include <iostream> 
using namespace std; 

void swap(int &a,int &b) 
{
    int temp; 
    temp=a; 
    a=b; 
    b=temp;
    cout<<*a<<","<<*b<<endl;
}

int main()
{
    int x=l; 
    int y=2; 
    swap(x,y);
    cout<<x<<","<<y<<endl; 
    
    return 0;
}

程序的输出结果:

2,1
2,1

(4) 全局变量传递

这里的“全局”变量并不见得就是真正的全局的,所有代码都可以直接访问的,只要这个变量的作用域足够这两个函数访问就可以了,比如一个类中的两个成员函数可以使用一个成员变量实现参数传递,或者使用static关键字定义,或者使用namespace进行限制等,而这里的成员变量在这种意义上就可以称为“全局”变量。


8.3 重载与覆盖有什么区别?

重载是指函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。在同一可访问区域内被声明的几个具有不同参数列 (参数的类型、个数、顺序不同)的同名函数,程序会根据不同的参数列来确定具体调用哪个函数。对于重载函数的调用,在编译期间就已经确定,是静态的,它们的地址在编译期间就绑定了,与多态无关。注意,重载不关心函数的返回值类型。

double calculate(double);
float calculate(double);

上面的两个同名函数只是返回值不同,所以不能构成重载。

成员函数被重载的特征如下:

(1) 相同的范围(在同一个类中)。

(2) 函数名字相同。

(3) 参数不同。

(4) virtual关键字可有可无。

覆盖是指派生类中存在重新定义基类的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,它和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针, 动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的。

覆盖的特征如下:

(1) 不同的范围(分别位于派生类与基类)。

(2) 函数名字相同。

(3) 参数相同。

(4) 基类函数必须有virtual关键字。

重载与覆盖的区别如下:

(1) 覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。

(2) 覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。

(3) 覆盖要求参数列表相同;重载要求参数列表不同。

(4) 覆盖关系中,调用方法体是根据对象的类型(对象对应存储空间类型)来决定的,重 载关系是根据调用时的实参表与形参表来选择方法体的。

程序示例如下:

#include <iostream> 
using namespace std;

class Base 
{
public:
    void f(int x)
    {
        cout<<"Base::f(int)"<<x<<endl;
    }
    
    void f(float x)
    {
        cout<<"Base::f(float)"<<x<<endl;
    }
    
    virtual void g(void)
    {
        cout<<"Base::g(void)"<<endl;
    }
};

class Derived:public Base
{
public:
    virtual void g(void)
    {
        cout<<"Derived::g(void)"<<endl;
    }
};

int main()
{
    Derived d;
    Base *pb = &d; 
    pb->f(42);
    pb->f(3.14f);
    Pb->g(); 
    
    return 0;
}

程序输出结果:

Base::f(int) 42 
Base::f(float) 3.14 
Derived: :g(void)

上例中,函数 Base::f(int)与 Base::f(float)相互重载,而 Base::g(void)被 Derived::g(void)覆盖。


8.4 隐藏与覆盖有什么区别?

隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

(1) 如果派生类的函数与基类的函数同名,但是参数不同,则不论有无virtual关键字, 基类的函数都将被隐藏。

(2) 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字,此时基类的函数被隐藏。

在调用一个类的成员函数时,编译器会沿着类的继承链逐级地向上查找函数的定义,如果找到了就停止查找了。所以,如果一个派生类和一个基类都存在同名(暂且不论参数是否相同)的函数,而编译器最终选择了在派生类中的函数,那么就说这个派生类的成员函数“隐藏”了基类的成员函数,也就是说它阻止了编译器继续向上查找函数的定义。

隐藏的特征如下:

(1) 必须分别位于派生类和基类中。

(2) 必须同名。

(3) 参数不同的时候本身已经不构成覆盖关系了,所以此时是否是virtual函数已经不重要了。

当参数相同的时候就要看是否有virtual关键字了,有的话就是覆盖关系,没有的时候就是隐藏关系了。

示例程序如下:

#include <iostream> 
using namespace std;

class Base
{
public:
    virtual float f(float x)
    {
        cout<<"Base::f(float) "<<x<<endl;
    }

    void g(float x)
    {
        cout<<"Base::g(float) "<<x<<endl;
    }

    void h(float x)
    {
        cout<<"Base::h(float) "<<x<<endl;
    }
};

class Derived:public Base
{
public:
    virtual float f(float x)
    {
        cout<<"Derived::f(float) "<<x<<endl;
    }

    void g(int x)
    {
        cout<<"Derived::g(int) "<<x<<endl;
    }   

    void h(float x)
    {
        cout<<"Derived::h(float) "<<x<<endl;
    }
};

int main()
{
    Derived d;
    Base *pb = &d;
    Derived *pd = &d;
    pb->f(3.14f);
    pd->f(3.14f);
    pb->g(3.14f);
    pd->h(3.14f);
    
    return 0;
}

程序输出结果:

Derived::f(float) 3.14 
Derived::f(float) 3.14 
Base::g(float) 3.14 
Derived::h(float) 3.14

上例中,函数 Derived: :f(float)覆盖了 Base: :f(float),函数 Derived: :g(int)隐藏了 Base: :g(float);函数Derived::h(float)隐藏了 Base::h(float)。


8.5 是否可以通过绝对内存地址进行参数赋值与函数调用?

同一个数可以通过不同的方式表达出来,对于函数的访问,变量的赋值除了直接对变量赋值以外,还可以通过绝对内存地址进行参数赋值与函数调用。

(1) 通过地址修改变量的值。

int x;
printf("%d\n",&x);
int *p = (int *)0x0012ff60;
*p = 3;
printf("%d\n",x);

程序首先输出变量x所在地址为十六进制的0x12ff60 (本来应该为8位的十六进制数,高位为0则省略掉),然后定义一个指针变量,让它指向该地址,通过指针变量的值来修改变量x的值。(实际测试发现每次编译器给x分配的地址都不同)

示例代码如下:

int *ptr=(int*)0xa4000000;
*ptr=Oxaabb;
printf("%d\n",*ptr);

以上程序会崩溃,因为这样做会给一个指针分配一个随意的地址,很危险,所以这种做法是不允许的。(必须是一个确定的地址)

(2) 通过地址调用函数的执行。

#include <iostream> 
using namespace std;

typedef void(*FuncPtr)();
void p()
{
    printf("MOP\n");
}

int main()
{
    void (*ptr)();
    P();
    printf("%x\n",p); 
    ptr = (void (*)())0x411Of0; 
    ptr();//函数指针执行 
    ((void (*)( ))0x4110f0)();
    ((FuncPtr)0x4110f0)(); 
    
    return 0;
}

程序输出结果:

MOP
4110f()
MOP
MOP
MOP

首先定义一个ptr的函数指针,第一次通过函数名调用函数,输出MOP,打印函数的入口地址,函数的入口地址为4110f()。然后给函数指针ptr赋地址值为p的入口地址,调用ptr,输 出MOP。接着的过程不通过函数指针直接执行,仍然使用p的入口地址调用,输出为MOP。最后是通过typedef调用的直接执行。

函数名称、代码都是放在代码段的,因为是放在代码段,每次会跳到相同的地方,但参数会压栈,所以函数只根据函数名来获取入口地址,与参数和返回值无关。无论参数和返回值如何不同,函数入口地址都是一个地方。


8.6 默认构造函数是否可以调用带参构造函数?

默认构造函数不可以调用单参数的构造函数。程序示例如下:

class A
{
public:
    A()
    {
        A(0);
        Print();
    }

    A(int j):i(j)
    {
        printf("Call A(int j)\n");
    }

    void Print()
    {
        printf("Call Print()!\n");
    }

    int i;
};

int main()
{
    A a;
    cout<<a.i<<endl; 
    return 0;
}

程序输出结果:

Call A(int j)
Call Print()
-858993460

以上代码希望默认构造函数调用带参构造函数,可是却未能实现。因为在默认构造函数内部调用带参的构造函数属用户行为而非编译器行为,它只执行函数调用,而不会执行其后的初 始化表达式。


8.7 C++中函数调用有哪几种方式?

由于函数调用经常会被嵌套,在同一时刻,堆栈中会存储多个函数的信息,每个函数又占用一个连续的区域,一个函数占用的区域常被称为帧(frame),编译器是从高地址开始使用堆栈的,在多线程(任务)环境,CPU的堆栈指针指向的存储器区域就是当前使用的堆栈。切换线程的一个重要工作,就是将堆找指针设为当前线程的堆栈栈顶地址。

当一个函数被调用时,进程内核对象为其在进程的地址空间的堆栈部分分配一定的栈内存给该函数使用,函数堆栈用于:

(1) 在进入函数之前,保存“返回地址”和环境变量。返回地址是指该函数结束后,从进入该函数之前的那个地址继续执行下去。

(2) 在进入函数之后,保存实参或实参复制、局部变量。

函数原型:[连接规范]函数类型[调用约定]函数名参数列表{……}

调用约定:调用约定是决定函数实参或实参复制进入和退出函数堆栈的方式以及函数堆栈释放的方式,简单地讲就是实参或实参复制入栈、出栈、函数堆栈释放的方式。在Win32下 有以下4种调用:

(1) _cdecl:它是C/C++的默认调用方式。实参是以参数列表从右依次向左入栈,出栈相反,函数堆栈由调用方来释放,主要用在那些带有可变参数的函数上,对于传送参数的内存栈是由调用者来维护的。

(2) _stdcall:它是_ API的调用约定,其实COM接口等只要是申明定义接口都要显 示指定其调用约定为_stdcall。实参以参数列表从右依次向左入栈,出栈相反。函数堆栈是由被调用方自己释放的。但是若函数含有可变参数,那么即使显示指定_stdcall,编译器也会自动把其改变成__cdecl。

(3) _thiscall:它是类的非静态成员函数默认的调用约定,其不能用在含有可变参数的函数上,否则编译会出错。实参以参数列表从右依次向左入栈,出栈相反。但是类的非静态成员函数内部都隐含有一个this指针,该指针不是存放在函数 堆栈上,而是直接存放在CPU寄存器上。

(4) _fastcall:快速调用。它们的实参并不是存放在函数堆栈上,而是直接存放在CPU寄存器上,所以不存在入栈、出栈、函数堆栈释放。

需要注意的是,全局函数或类静态成员函数,若没指定调用,约定默认是_cdecl,或是IDE 设置的。


8.8 什么是可重入函数?C++中如何写可重入函数?

可重入函数是指能够被多个线程“同时”调用的函数,并且能保证函数结果正确性的函数。

在c语言中编写可重入函数时,尽量不要使用全局变量或静态变量,如果使用了全局变量或静态变量,就需要特别注意对这类变量访问的互斥。一般采用以下几种措施来保证函数的可重入性:信号量机制、关调度机制、关中断机制等方式。

需要注意的是,不要调用不可重入的函数,当调用了不可重入的函数时,会使该函数也变 为不可重入的函数。一般驱动程序都是不可重入的函数,因此在编写驱动程序时一定要注意重入的问题。


9 数组

数组的下标问题、越界问题一直是程序员经常忽视的问题,但与数组相关的问题却不仅限于此:多维数组的缺省赋值、下标越界、行存储、列存储等,本节将详细对数组的这些问题进行逐一分析。

9.1 int a[2][2]= { {1}, {2,3} },则a[0][1]的值是多少?

二维数组的初始化一般有两种方式,第一种方式是按行来执行(如int array[2][3]= { {0,0,1}, {1,0,0} };),而第二种方式是把数值写在一块(如int array[2][3]= { 0,0,1,1,0,0 };)。

若只对部分元素进行初始化,数组中未赋值的元素自动为赋值为0,所以a[0][1]的值是0。


9.2 a是数组,(int*)(&a+1)表示什么意思?

表示int类型的数组指针,若a为数组a[5],则 (int*)(&a+1) 为a[5]。

示例程序如下:

#include <stdio.h> 

int main()
{
    int a[5]={1,2,3,4,5};
    int b[ 100];
    int *ptr=(int*)(&a+1); 
    printf("%d\n%d\n",*(a+1),*(ptr-1)); 
    printf("sizeof(b)=%d\n",sizeof(b)); 
    printf("sizeof(&b)=%d\n",sizeof(&b)); 
    
    return 0;
}

程序输出结果:

2
5
sizeof(b)=400
sizeof(&b)=4

&a是数组指针,是一个指向int (*)[5]的指针,所以&a+1的地址是&a地址再加5*sizeof(int),它的运算单位是int(*)[5]。经过类型转换后,ptr相当于int *[5],也就是指向了一个含有5个元素的数组。

ptr-1的单位是ptr的类型,因此ptr-1的位置刚好是a[4],它在内存中的分布位置是和 &a+1相邻的。但是ptr与(&a+l)类型是不一样的,所以ptr-1只会减去sizeof(int*)。

值得注意的是,a和&a的地址是一样的,但意思不一样,a是数组首地址,也就是a[0]的地址;&a是 对象(数组)首地址;a+1是数组下一元素的地址,即a[1];而&a+1是下一个对象的地址, 即 a[5]。

9.3 不使用流程控制语句,如何打印出1 ~ 1000的整数?

采用构造函数与静态构造变量结合的方法实现。首先在类中定义一个静态成员变量,然后在构造函数里面打印该静态变量的值,并对静态变量进行自增操作,同时在主函数里面定义一个类数组,程序代码示例如下:

class print
{
public:
    static int a;

    print()
    {
        printf("%d\n",print::a);
        a++;
    }
};
int print::a = 1;

int main()
{
    print tt[100]; 
    
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/linuxAndMcu/p/10199546.html