[C++基础] 变量、关键字、运算符、位操作篇
一、变量篇
1 全局变量和静态变量有什么异同?
相同:都在静态存储区分配空间,生命周期与程序生命周期相同。
区别:全局变量的作用域是整个程序,它只需要在一个源文件中定义,就可以作用于所有的源文件。而静态变量只在定义其的源文件内有效。
2 变量定义与变量声明有什么区别?
定义(definition)为变量分配存储空间,还可以为变量指定初始值。而声明(declaration)是指向程序表明变量的类型和名字。 定义也是声明,定义变量的同时也声明了它的类型和名字。一般为了叙述方便,把建立存储空间的声明称定义,而不把建立存储空间的声明称为声明。
3 C 语言中各种变量的默认初始值是什么?
全局变量放在内存的全局数据区,如果在定义的时候不初始化,则系统将自动为其初始化,数值型为0,字符型为空,指针变量也被赋值为NULL。静态变量的情况与全局变量类似。而非静态局部变量如果不显示初始化,那么其内容是不可预料的,将是随机数,会很危险,对系统的安全造成非常大的隐患。
4 如何判断一段程序是 C 编译程序还是 C++ 编译程序编译的?
如果编译器在编译 cpp 文件,那么 _cplusplus 就会被定义,如果是一个 C 文件在被编译, 那么 _STDC_ 就会被定义。_STDC_ 是预定义宏,当它被定义后,编译器将按照 ANSIC 标准来编译 C 语言程序。所以可以采用如下程序进行判断:
#ifdef _cplusplus
#define USING_CPP 1
#else
#define USING_CPP 0
#endif
#include <stdio.h>
int main()
{
if(USING_CPP)
printf("C++\n");
else
printf("C\n");
return 0;
}
5 C 语言中 int 和 float 有什么区别?
主要有如下三个区别:
(1)表示的数据范围不同。C 语言中的 int 变量通常的表示范围为-2147483648~2147483647
,也就是-2^31
到2^31
之间。而-3.4E+38 ~ 3.4E+38
则是 float 类型表示的数据范围。float 表示的数据范围要大于 int 表示的数据范围。
(2)变量赋值方法不同。C 语言中,将 i 设定为一个 int 变量并赋值的方法为:int i=xx;
,其中 xx 为一个整数,例如 3、4、5,不可以是小数。将 i 设定为一个 float 变量的方法为:float i=yy
;,其中 yy 为一个浮点型数,可以带上小数点,例如 3.0、4.5、5.7 等等。
(3) 字节构成不同。int 和 float 类型在计算机中都占 4 个字节,但是 float 类型的 4 个字节构成为包括一个符号位、一个 8 位二进制指数和一个 23 位尾数, 以指数形式存储,而 int 类型的 4 个字节构成全部为整数。
扩展: 为什么有些int是float表示不了的呢?
因为 int 与 float 同样占4个字节,float 表示的范围又比 int 大并且还包含很多小数,那 int 的每个值都能被 float 表示就是不可能的事情了。
参考:int、unsigned int、float、double和char在内存中存储方式
6 int为什么在32位系统中是4个字节?
32 位系统对应的 CPU 是32位的,为了和 CPU 的字宽一致,提高处理速度。
7 为什么一个指针在32位系统中占4个字节,在64位系统中占8个字节?
可以参考:
为什么一个指针在32位系统中占4个字节,在64位系统中占8个字节?
二、C/C++ 关键字
1 const 有哪些作用?
(1)定义常量,使其值不可被修改,另外使编译器可以对其进行类型检查;
(2)修饰函数形参,防止值被意外的修改,提高程序的健壮性;
(3)修饰函数返回值,使返回值不能被修改;
(4)修饰常量指针(const char *p)和指针常量(char * const p);
(5)在 C++ 中,修饰类成员函数,任何不会修改数据成员的函数都应该用 const 修改,以及修饰类成员数据。
2 extern 有哪些作用?
extern 有两种作用,下面分别详细介绍一下。
1. 在模块外使用全局变量
extern 可以置于变量或者函数前,如在头文件中:extern int g_val;
,其声明的函数和变量可以在其他模块中使用,记住它是一个声明不是定义。也就是说 B 模块(编译单元)要是引用模块(编译单元) A 中定义的全局变量或函数时,它只要包含 A 模块的头文件即可。
2. 在 C++ 环境下使用 C 函数
C++ 语言是一种面向对象编程语言,支持函数重载,而 C 语言是面向过程的编程语言, 不支持函数重载,所以函数被 C++ 编译后在库中的名字与 C 语言的不同。如果声明一个 C 语言函数float f(int a,char b)
,C++ 的编译器就会将这个名字变成像 _f_int_char 之类的东西以支持函数重载。然而 C 语言编译器的库一般不执行该转换,所以它的内部名为 _f,这样连接器将无法解释 C++ 对函数 f() 的调用。
C++ 提供了 C 语言符号extern “C”
来解决名字匹配问题,extern 后跟一个字符串来指定想声明的函数的连接类型,后面是函数声明。
extern "C" float f(int a,char b);
该语句的目的是告诉编译器 f() 是 C 连接的,这样 C++ 就不会转换函数名。应该到库中找名字 _f 而不是找 _f_int_char。 C++ 编译器开发商已经对 C 标准库的头文件作了 extern “C” 处理,所以可以用 include 直接引用这些头文件。
3 static 变量有哪些作用?
(1)在函数体内,被声明为静态的变量只初始化一次,以后该函数再被调用,将不会再初始化,这就使变量具有 “记忆” 功能。
(2)在模块内(但在函数体外),如果把一个变量或者函数声明为静态的,那么可以将其作用域被限制在本模块内,起一个 “隐藏” 的作用,避免命名冲突。
(3)默认初始化为 0,因为静态变量存储在静态数据区,而静态数据区中的所有字节默认值都是 0,某些时候这一特点可以减少程序员的工作量。比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 ‘\0’ 太麻烦。如果把字符数组定义成静态的,就省去了这个麻烦。(全局变量也存储在静态数据区)
(4)在 C++ 中,在类中声明 static 变量或者函数。其初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:
- 类的静态成员函数是属于整个类而非类的对象,所以它没有 this 指针,这就导致了它仅能访问类的静态数据和静态成员函数;
- 不能将静态成员函数定义为虚函数;
- 由于静态成员函数没有 this 指针,所以就差不多等同于 nonmember 函数,结果就产生了一个意想不到的好处:成为一个 callback 函数,使得我们得以将 C++ 和 C-based X Window 系统结合,同时也成功的应用于线程函数身上。
4 new/delete 与 malloc/free 的区别是什么?
(1)new/delete 是操作符,而 malloc/free 是函数,在C语言中需要 <stdlib.h> 的支持;
(2)new 能够自动计算需要分配的内存空间,而 malloc 需要手工计算字节数;
(3)new 与 delete 直接带具体类型的指针,而 malloc 与 free 返回的是 void 类型的指针;
(4)new 是类型安全的,而 malloc 不是,例如int* p = new float[2]
,编译时就会报错; 而int* p = malloc(2 * sizeof(float))
,编译时编译器就无法指出错误来;
(5)new 将调用构造函数,而 malloc 不能;delete 将调用析构函数,而 free 不能。
5 断言 ASSERT() 是什么?
ASSERT() —般被称为断言,它是一个调试程序时经常使用的宏。它定义在 <assert.h> 头文件中,通常用于判断程序中是否出现了非法的数据,在程序运行时它计算括号内的表达式的值。如果表达式的值为 false(0),程序报告错误,终止运行,以免导致严重后果,同时也便于查找错误;如果表达式的值不为 0,则继续执行后面语句。其用法如下:
ASSERT(n != 0); // 分母为0,为非法数据,表达式为false,程序报告错误,程序会终止运行
k = 10 / n;
需要注意的是,ASSERT()只在 Debug 版本中有,编译的 Release 版本则被忽略。还需要注意的一个问题是 ASSERT() 与 assert() 的区别,ASSERT() 是宏,而 assert() 是ANSI C标准中规定的函数,它与 ASSERT() 的功能类似,但是可以应用在 Release 版本中。
三、C/C++ 运算符
1 前置运算与后置运算有什么区别?
以 ++ 操作为例,对于变量 a, ++a 表示先增加内存中 a 的值,然后再把值放在装入寄存器中;a++ 表示先把 a 的值装入寄存器,然后再增加内存中 a 的值。
一般而言,当涉及表达式计算时,++a 是先将值增加 1,再返回其值;而 a++ 是先返回其值,再增加1。
2 a是变量,执行 (a++) += a 语句是否合法?
不合法。a++ 不能当做左值使用。++a 可以当左值使用。a++ 是先把a的值装入寄存器,然后再增加内存中 a 的值,此时左值是 a 的值,而值不能作为左值,所以非法。而 ++a 是先增加内存中 a 的值,然后再把值放在装入寄存器中,此时左值是 a,所以合法。
3 *p++与(*p)++等价吗?为什么?
因为优先级顺序的问题,*p++
与(*p)++
并不等价,前者先完成取值操作,然后对指针地址执行 ++ 操作;而后者先完成取值操作,然后对该值进行 ++ 运算。
四、位操作
1.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 %d %d %d\n",t.a, t.b, t.c, sizeof(test)); // 1 -1 -1 4
return 0;
}
由于 a 占两位,而 a 被赋值为 1,二进制就是 01,因此 %d 输出的时候输出 1;b 也占了两位,赋值为 3,二进制也就是 11,由于使用了 %d 输出,表示的是将这个 b 作为有符号 int 型来输出,这样的话二进制的 11 将会有一位被认为是符号位,并且两位的 b 也会被扩展为 int 类 型,也就是4字节,即 32 位。
1.2 如何实现位操作求两个数的平均值?
一般而言,求解平均数的方法就是将两者相加,然后除以 2,以变量 x 与 y 为例,两者的平均数为 (x+y)/2。
但是采用上述方法,会存在一个问题,当两个数比较大时,如两者的和大于了机器位数能够表示的最大值,可能会存在数据溢出的情况,而采用位运算方法则可以避免这一问题,而且位运算相比除法运算, 效率更高。
示例程序如下:
#include <stdio.h>
int main()
{
int x = 2147483647, y = 2147483647;
printf("%d\n",(x+y)/2); // -1
printf("%d\n",(x&y)+((x^y)>>1)); // 2147483647
return 0;
}
1.3 如何求解整型数的二进制表示中 1 的个数?
方法一,程序代码如下:
#include <stdio.h>
int func (int n)
{
int count=0;
while (n)
{
count += n & 0x1u ;
n >>= 1 ;
}
return count;
}
int main()
{
printf("%d\n",func(9999)); // 8
return 0;
}
判断每个数的二进制表示中每一位是否为 1,如果为 1,就在 count 上加 1,而循环的次数是常数,即 n 的位数。但该方法有一个缺陷,就是在 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)); // 8
return 0;
}
为了理解这个算法的核心,需要理解以下两个操作:
(1)当一个数被减 1 时,它最右边的那个值为 1 的 bit 将变为 0,同时其右边的所有的 bit 都会变成 1;
(2)“&=”,位与并赋值操作。去掉已经被计数过的 1,并将该值重新设置给 n。这个算法循环的次数是 bit 位为 1 的个数,对 bit 为 1 比较稀疏的数来说,性能很好。
1.4 不能用 sizeof() 函数,如何判断操作系统是 16 位还是 32 位的?
方法一:一般而言,机器位数不同,其表示的数字的最大值也不同,根据这一特性,可以判断操作系统的位数。例如,运行如下代码:
#include <stdio.h>
int main()
{
int i = 65536;
printf("%d\n",i); // 16位机器下输出0,32位机器下输出65536
int j = 65535;
printf("%d\n",j); // 16位机器下输出-1,32位机器下输出65535
return 0;
}
由于 16 位机器下,能够表示的最大数为65535,会出现越界情况。而在 32 位机器下,则不会出现溢出的情况,所以输出为正常输出。
方法二:对 0 值取反,不同位数下的 0 值取反,其结果不一样。运行如下代码:
#include <stdio.h>
int main()
{
unsigned int a =〜0;
if( a>65536)
printf("32 位\11");
else
printf(”16 位\11");
return 0;
}
1.5 嵌人式编程中,什么是大端?什么是小端?
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
大小端在内存中的存放方式举例
一个 16bit 的 short 型 x,在内存中的地址为 0x0010,x 的值为 0x1122。那么 0x11 为数据高字节,0x22 为数据低字节。
- 对于大端模式,就将 0x11 放在内存低地址中,即 0x0010 中;0x22 放在内存高地址中,即 0x0011 中。
- 小端模式,就将 0x11 放在内存高地址中,即 0x0011 中;0x22 放在内存低地址中,即 0x0010 中。
我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的 ARM,DSP 都为小端模式。有些 ARM 处理器还可以由硬件来选择是大端模式还是小端模式。
引申:如何判断计算机处理器是大端还是小端?
方法一:可以通过指针地址来判断,由于在32位计算机系统中,int 占 4 个字节,char 占 1 个 字节,所以可以采用如下做法实现该判断。
int fun()
{
int num = 0x12345678;
// (char*)&num获得num的起始地址,*((char*)&num)是num的起始地址所指向的值。
return (*((char*)&num) == 0x12 )?1:0; // 本机返回1,为大端 返回0,为小端
}
方法二:联合体union的存放顺序是所有成员都从低地址开始存放。
// 判断系统是大端还是小端:通过联合体,因为联合体的所有成员都从低地址开始存放
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);
}