【C++复习】易错的小问题

1. 数组初始化

数组初始化有很多坑,比如

int a [10]; 

这样的话,10个元素的值都是没有初始化的,在32位Windows上是-858993460,16进制表示是0xcccccccc之类的垃圾数据,栈中变量如果没有初始化,值就是这个。如果要初始化数组,可以用

int a[10] = {}; 

或者

int a[10] = {0};

两种都是把值初始化为0。需要注意的是,这种方法只能把值初始化为0,如果

int a[10] = {123}; 

数组中也只有第0个是123,其余还是0.

2. 使用指针还是引用作为参数的一个重要区分:
引用必须被初始化为指向一个对象,一旦初始化了,它就不能再指向其他对象,而指针可以指向一系列不同的对象也可以什么都不指向
所以,如果一个参数可能在函数中指向不同的对象,或者这个参数可能不指向任何对象,则必须使用指针参数

3.函数模板调用时不需要显式指定类型,系统自动匹配参数类型,若没有合适的,会进行报错。而类模板使用需要显式指定类型。

template<typename T> 

T add(T a, T b)

{

retrun a + b ; 

}

int main ()

{

cout << add(1, 2) ; // 可以,不需要add<int>(1,2)

}

4. 注意函数调用时,参数的入栈顺序是从右到左。

函数func的定义如下:
1
2
3
4
5
voidfunc(constint& v1, cont int& v2)
{
    std::cout << v1 << ' ';
    std::cout << v2 << ' ';
}
 以下代码输出结果为____。
1
2
3
4
5
6
intmain (intargc, char* argv[])
{
    inti=0;
    func(++i,i++);
    return0;
}
答案是 2, 0

5. 要注意类型的表示范围。

chara=101;
intsum=200;
a+=27;sum+=a;
printf("%d\n",sum);
答案:72
6. 静态变量(包含全局变量,类的静态成员等)在main之前调用构造函数,在main之后调用析构函数。
7. 如何重载一元运算符

因为++和--有前缀和后缀两种形式,为了区分,要求在后缀形式加一个int参数。  const Fraction operator ++(int)   中 int不过是个哑元(dummy),是永远用不上的,它只是用来判断++是prefix  还是  postfix  。如果有哑元,则是postfix,否则,就是prefix 。 
(1) 双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。
(2) 前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。
(3) 后置单目运算符重载为类的成员函数时,函数要带有一个整型形参。
8. 时刻注意指针大小和系统位数有关,64位是8字节,32位是4字节。

9. 赋值语句做表达式时,返回赋值后的值。如int a = (b = 10); 结果 a = 10 ;

10. 申请的内存一定要释放,打开的文件也要关闭

malloc -- free

fopen -- fclose

以下代码段有问题的是()

正确答案: A B C   你的答案: A B C (正确)

<pre>void func1(char *e){
  char *p1;
  p1=malloc(100);
  sprintf(p1,error:"%s'.",e);
  local_log(p1);
  }
</pre>
<pre>
int func2(char *filename)
  {
    FILE *fp;
    int key;
    fp=fopen(filename,"r");
    fscanf(fp,"%d",&key);
    return key;
   }
</pre>
<pre>
void func3(char *info){
  char *p,*pp;
  p=malloc(100);
  pp=p;
  free(p);
  sprintf(pp,*info:"%s'.",info);
  free(pp);
  }
</pre>
  答案是A 申请了空间没有释放,答案B 打开流没有关闭,答案C free了二次,但是其实指针都是一块内存空间

11. C++里,重写就是覆盖,如果没有virtual就是隐藏。这些术语和C#有点不一样。

12. C++ 实现接口是通过只有纯虚函数的抽象基类。

13. 二维数组初始化有两种方式:一种顺序初始化, int array1[3][2]={4,2,5,6};//顺序初始化;另一种按行初始化,如 int array2[3][2]={{4,2},{5},{6}};//按行初始化
本题是顺序初始化,初始化的元素个数不超过一行规定的个数,所以只有一行。

14. 全局变量只声明就会定义,而局部变量只声明,值会是一个0xcccccccc之类的值(VC++),比如:http://blog.csdn.net/u012861978/article/details/46560651

int a ;

int main()

{

int a2; 
}

//结果是

a 为 0; a2为0xcccccccc

15. http://blog.csdn.net/dawn_sf/article/details/68078882

以下叙述中正确的是(    )

正确答案: B D   你的答案: A B (错误)

在C++中数据封装是通过各种类型来实现的
在C++中,数据封装可以由struct关键字提供
数据封装就是使用结构类型将数据代码连接在一起
数据封装以后,仍然可以不通过使用函数就能直接存取数据


A  C++通过类来实现封装性,把数据和与这些数据有关的操作封装在一个类中,或者说,类的作用是把数据和算法封装在用户声明的抽象数据类型中。

B 正确,C++中虽然struct的默认封装类型为public,但是你也可以设置为private的形式,都可以.

所有的 C++ 程序都有以下两个基本要素:
  • 程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
  • 程序数据:数据是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念(并不是单纯将数据代码连接起来,是数据和操作数据的函数.),这样能避免受到外界的干扰和误用,从而确保了安全。

D 正确  一般情况好像是不允许的. 但是如果你用一些比较骚的方法. 前提要你知道一个类的结构分布,比如让你访问类中虚函数表的内容,你就可以先取到类的首地址,把指针强转成int*,通过你了解到的结构定位到虚函数表的地址,这样你就能访问到虚函数表里面的元素,当然这些的前提都是你对这个类的结构相当了解. 一般不推荐使用,因为做法比较bug. 举个例子吧,下面是打印一个虚函数表的实例.
代码:
class Base
{

public:
virtual void func1()
{
cout << "Base::func1" << endl;
}

virtual void func2()
{
cout << "Base::func2" << endl;
}

private:
int a;

};

class Derive :public Base
{

public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}

virtual void func3()
{
cout << "Derive::func3" << endl;
}

virtual void func4()
{
cout << "Derive::func4" << endl;
}

private:
int b;

};


typedef void(*FUNC)(void);
void PrintVTable(int* VTable)
{
cout << " 虚表地址" << VTable << endl;

for (inti = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}

cout << endl;
}
int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1)));
system("pause");
return0;
}

这里重要的就是这个传参,上面那个打印虚函数表的函数很容易理解的.
PrintVTable((int*)(*(int*)(&d1)));
首先我们肯定要拿到d1的首地址,把它强转成int*,让他读取到前4个字节的内容(也就是指向虚表的地址),再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int*的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。最后我们来看看输出结果:



当我们能打印出它的时候,修改也就变得容易许多.最后解释一下打印虚函数表的函数.


16. 正数原码、反码、补码形式一致。 负数反码,为其原码的符号位不变,其他位取反; 负数补码,是其反码加1。

原码是值,

补码是内部存储格式,

反码是原码取反,其中正数的反码等于原码

https://www.nowcoder.com/test/question/done?tid=10736459&qid=26145#summary

Pandora的答案

不管是在32还是在64位编译器处理下,int都是4字节32位,所以整数范围是-2147483648~2147483647,数值以补码形式存储,但要求值还是得自己换成原码正数的原码、补码、反码相同,原码即补码,补码即原码。

-i 是对 i 求补码,(含符号位)按位取反再加1.

~i 是对 i 求反码

原码 -> 补码 是 按位取反加1

补码 -> 原码 是 先减一在(除符号位)按位取反

17. 二元操作符+定义为类成员函数数时因为有隐藏的this指针,故只需用一个形参;如果定义为普通非成员函数,则需要使用两个形参,此时应该选A,c++primer中提到最好将算术运算符定义为非成员,所以如果没有类成员的限制的话,最好的答案为A

18. 引用不能是void的,比如void& n,这是错的。“不允许使用对void的引用”

19. 对于0xcccccccc和0xcdcdcdcd,在 Debug 模式下,VC 会把未初始化的栈内存上的指针全部填成 0xcccccccc ,当字符串看就是 “烫烫烫烫……”;会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd,当字符串看就是 “屯屯屯屯……”。那么调试器为什么要这么做呢?VC的DEBUG版会把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,如果野指针的初值不确定,那么每次调试同一个程序就可能出现不一样的结果,比如这次程序崩掉,下次却能正常运行,这样显然对我们解bug是非常不利的,所以自动初始化的目的是为了让我们一眼就能确定我们使用了未初始化的野指针了。

对于0xfeeefeee,是用来标记堆上已经释放掉的内存。注意,如果指针指向的内存被释放了,变量变量本身的地址如未做改动,还是之前指向的内存的地址。如果该指针是一个类的指针,并且类中包含有指针变量,则内存被释放后(对于C++类,通常是执行delete操作),类中的指针变量就会被赋值为0xfeeefeee。如果早调试代码过程中,发现有值为0xfeeefeee的指针,就说明对应的内存被释放掉了,我们的代码已经出问题了。

20. 虚函数可以内联inline https://www.zhihu.com/question/45894112

21. STL中的容器,只要支持随机访问就支持[](重写了operator[])

22.  虚函数不能声明为静态的、全局的、友元的。

23. 

};
ClassC aObject;
ClassA* pA = &aObject;
ClassB* pB = &aObject;
ClassC* pC = &aObject;

下面那一个语句是不安全的

正确答案: A B C   你的答案: B (错误)

delete pA
delete pB
delete pC
因为三个指针不是动态建立的,不用删除,只有用new建立的对象才会用到delete删除

24. 以下程序输出是____。

1
2
3
4
5
6
7
8
9
10
#include <iostream> 
using namespace std; 
intmain(void
    constinta = 10
    int* p = (int*)(&a); 
    *p = 20
    cout<<"a = "<<a<<", *p = "<<*p<<endl; 
    return0

正确答案: D   你的答案: E (错误)

编译阶段报错运行阶段报错
a = 10, *p = 10
a = 20, *p = 20
a = 10, *p = 20
a = 20, *p = 10

因为a 和p都指向相同的内存地址,所以输出的前两个结果是相同的,但为啥相同的内存里的结果不相同么?--这就是常量折叠.

这个"常量折叠"是 就是在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。可以算作一种编译优化。

因为编译器在优化的过程中,会把碰见的const全部以内容替换掉(跟宏似的: #define pi 3.1415,用到pi时就用3.1415代替),这个出现在预编译阶段;但是在运行阶段,它的内存里存的东西确实改变了!!!
简单的说就是,当编译器处理const的时候,编译器会将其变成一个立即数。 

25. 函数是构成C语言的基本单位

26. 逗号表达式的计算顺序是从左往右

27. fopen() 函数中,

       r 打开只读文件,该文件必须存在。

  r+ 打开可读写的文件,该文件必须存在。

  rb+ 读写打开一个二进制文件,只允许读写数据。

  rt+ 读写打开一个文本文件,允许读和写。

  w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。

  w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。

  a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)

  a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)

  wb 只写打开或新建一个二进制文件;只允许写数据。

  wb+ 读写打开或建立一个二进制文件,允许读和写。

  wt+ 读写打开或着建立一个文本文件;允许读写。

  at+ 读写打开一个文本文件,允许读或在文本末追加数据。

  ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。

  上述的形态字符串都可以再加一个b字符,如rb、w+b或ab+等组合,加入b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。不过在POSIX系统,包含Linux都会忽略该字符。由fopen()所建立的新文件会具有S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)权限,此文件权限也会参考umask 值。

28. 使用 inline 关键字的函数只是用户希望它成为内联函数,但编译器有权忽略这个请求,比如:若此函数体太大,则不会把它作为内联函数展开的。类内的成员函数,默认都是 inline 的。【定义在类声明之中的成员函数将自动地成为内联函数】

https://www.nowcoder.com/test/question/done?tid=10748635&qid=44774#summary

29. C语言中只有按值传递。传递方式只有两种1. 按值,2. 按引用,没有按指针传递的说法。传指针也是按值传指针变量。

30. switch 语句的执行过程, 先去查有没有符合的case标签,如果没有则跳到default后运行,case和default的相对位置不会影响这个过程。

31. case 语句的条件必须是 常量。

32. free释放的内存不一定直接还给操作系统,可能要到进程结束才释放。

33. 可以直到malloc不能直接申请物理内存,它申请的是虚拟内存

34. int (*a[10])(int); //函数指针的数组的声明方式,指向有一个参数并且返回类型均为int的函数的数组。https://cdecl.org/ 这个网站可以自动分析声明

https://www.nowcoder.com/test/question/done?tid=10749995&qid=36725#summary

========== 1. 由变量描述到表达式 ==========
本题
用变量a给出下面的定义:一个有10个指针的数组,该指针指向一个函数,该函数有一个整形参数并返回一个整型数:

1. 定义一个数组
a[10]
2. 该数组元素是指针
*a[10]
3. 该指针指向一个函数,
(* a[10]) ()
4. 有一个整形参数
(* a[10]) (int)
5. 并返回一个整形数
int (* a[10]) (int)

另外一个例子:
定义一个函数指针,指向的函数有两个int形参并且返回一个函数指针,返回的指针指向一个有一个int形参且返回int的函数?
来自 <http://www.nowcoder.com/questionTerminal/960f8047a9ee4a6f8227768f3bc2734d>

1. 定义一个函数指针
(*p)
2. 指向的函数有两个int参数
(*p) (int,int)
3. 返回值是一个函数指针
* (*p) (int,int) (* (*p) (int,int))
4. 返回的指针指向一个int参数
(* (*p) (int,int))(int)
5. 并返回 int
int (* (*p) (int,int))(int)
所以:int (* (*p) (int,int))(int)

相似的一个例子
声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是()

来自 <http://www.nowcoder.com/questionTerminal/242d747044bd417e95fe37d69884dff8>
1. 声明一个指针
(*p)
2. 该指针指向一个数组
(*p)[10]
3. 每个元素是一个函数指针
(*(*p)[10])()
4. 参数是int*
(*(*p)[10])(int*)
5. 返回值是int
int (*(*p)[10])(int*)

========== 2. 由表达式到变量描述 ==========

经典例子
注意: 指向数组的指针和指针数组
int *a[10]: a是一个数组,该数组的元素是指针,每个指针都指向一个int型
int (*a)[10]:a是一个指针,该指针指向一个数组,数组元素是int

From http://cdecl.org/
int (*a)[10]: declare a as pointer to array 10 of int
int *a[10]:declare a as array 10 of pointer to int

例子1
int (*a[10])(int)
1. a是一个数组
2. a这个数组的元素是一个指针
3. 指针是一个函数指针
4. 这个函数参数是int,返回值是int

a是一个包含10个元素的数组,每个元素指向一个参数是int,返回值是int的函数。

From http://cdecl.org/
declare a as array 10 of pointer to function (int) returning int
-----------
例子2
int (*(*p)[10])(int*)
1. p是一个指针
2. p这个指针指向一个数组
3. 数组元素是指针
4. 数组元素的指针指向一个函数
5. 这个函数参数是int*,返回值是int

p是一个指向包含10个元素的数组的指针,每个元素是一个函数指针,指向一个参数是int*,返回值是int的函数。
declare p as pointer to array 10 of pointer to function (pointer to int) returning int

35. 运算符优先级以及计算方法


优先级 操作符 描述 例子 结合性
1 ()
[]
->
.
::
++
--
调节优先级的括号操作符
数组下标访问操作符
通过指向对象的指针访问成员的操作符
通过对象本身访问成员的操作符
作用域操作符
后置自增操作符
后置自减操作符
(a + b) / 4;
array[4] = 2;
ptr->age = 34;
obj.age = 34;
Class::age = 2;
for( i = 0; i < 10; i++ ) ...
for( i = 10; i > 0; i-- ) ...
从左到右
2 !
~
++
--
-
+
*
&
(type)
sizeof
逻辑取反操作符
按位取反(按位取补) 
前置自增操作符
前置自减操作符
一元取负操作符
一元取正操作符
解引用操作符
取地址操作符
类型转换操作符
返回对象占用的字节数操作符
if( !done ) ...
flags = ~flags;
for( i = 0; i < 10; ++i ) ...
for( i = 10; i > 0; --i ) ...
int i = -1;
int i = +1;
data = *ptr;
address = &obj;
int i = (int) floatNum;
int size = sizeof(floatNum);
从右到左
3 ->*
.*
在指针上通过指向成员的指针访问成员的操作符
在对象上通过指向成员的指针访问成员的操作符
ptr->*var = 24;
obj.*var = 24;
从左到右
4 *
/
%
乘法操作符
除法操作符
取余数操作符
int i = 2 * 4;
float f = 10 / 3;
int rem = 4 % 3;
从左到右
5 +
-
加法操作符
减法操作符
int i = 2 + 3;
int i = 5 - 1;
从左到右
6 <<
>>
按位左移操作符
按位右移操作符
int flags = 33 << 1;
int flags = 33 >> 1;
从左到右
7 <
<=
>
>=
小于比较操作符
小于或等于比较操作符
大于比较操作符
大于或等于比较操作符
if( i < 42 ) ...
if( i <= 42 ) ...
if( i > 42 ) ...
if( i >= 42 ) ...
从左到右
8 ==
!=
等于比较操作符
不等于比较操作符
if( i == 42 ) ...
if( i != 42 ) ...
从左到右
9 & 按位与操作符 flags = flags & 42; 从左到右
10 ^ 按位异或操作符 flags = flags ^ 42; 从左到右
11 | 按位或操作符 flags = flags | 42; 从左到右
12 && 逻辑与操作符 if( conditionA && conditionB ) ... 从左到右
13 || 逻辑或操作符 if( conditionA || conditionB ) ... 从左到右
14 ? : 三元条件操作符 int i = (a > b) ? a : b; 从右到左
15 =
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
赋值操作符
复合赋值操作符(加法)
复合赋值操作符(减法)
复合赋值操作符(乘法)
复合赋值操作符(除法)
复合赋值操作符(取余)
复合赋值操作符(按位与)
复合赋值操作符(按位异或)
复合赋值操作符(按位或)
复合赋值操作符(按位左移)
复合赋值操作符(按位右移)
int a = b;
a += 3;
b -= 4;
a *= 5;
a /= 2;
a %= 3;
flags &= new_flags;
flags ^= new_flags;
flags |= new_flags;
flags <<= 2;
flags >>= 2;
从右到左
16 , 逗号操作符 for( i = 0, j = 0; i < 10; i++, j++ ) ... 从左到右

记忆方法:
--摘自《C语言程序设计实用问答》       
    问题:如何记住运算符的15种优先级和结合性?    
    解答:C语言中运算符种类比较繁多,优先级有15种,结合性有两种。    
    如何记忆两种结合性和15种优先级?下面讲述一种记忆方法。    
    结合性有两种,一种是自左至右,另一种是自右至左,大部分运算符的结合性是自左至右,只有单目运算符、三目运算符的赋值运算符的结合性自右至左。    
    优先级有15种。记忆方法如下:    
    记住一个最高的:构造类型的元素或成员以及小括号。    
    记住一个最低的:逗号运算符。    
    剩余的是一、二、三、赋值。    
    意思是单目、双目、三目和赋值运算符。    
    在诸多运算符中,又分为:    
    算术、关系、逻辑。    
    两种位操作运算符中,移位运算符在算术运算符后边,逻辑位运算符在逻辑运算符的前面。再细分如下:    
    算术运算符分     *,/,%高于+,-。    
    关系运算符中,〉,〉=,<,<=高于==,!=。    
    逻辑运算符中,除了逻辑求反(!)是单目外,逻辑与(&&)高于逻辑或(||)。    
    逻辑位运算符中,除了逻辑按位求反(~)外,按位与(&)高于按位半加(^),高于按位或(|)。    
    这样就将15种优先级都记住了,再将记忆方法总结如下:    
    去掉一个最高的,去掉一个最低的,剩下的是一、二、三、赋值。双目运算符中,顺序为算术、关系和逻辑,移位和逻辑位插入其中。

36.

C++中哪些函数不能声明为inline?

  1. 包含了递归、循环等结构的函数一般不会被内联。
  2. 虚拟函数一般不会内联,但是如果编译器能在编译时确定具体的调用函数,那么仍然会就地展开该函数。
  3. 如果通过函数指针调用内联函数,那么该函数将不会内联而是通过call进行调用。
  4. 构造和析构函数一般会生成大量代码,因此一般也不适合内联。
  5. 如果内联函数调用了其他函数也不会被内联。

如果想要阻止某函数被内联,可以在函数体前加上 __attribute__((noinline)) 。

37. 尽量不要再构造函数和析构函数中调用虚函数。
所谓虚函数就是多态情况下只执行一个,而从继承的概念来讲,总是要先构造父类对象,然后才能是子类对象,如果构造函数设为虚函数,那么当你在构造父类的构造函数时就不得不显示的调用构造,还有一个原因就是为了防错,试想如果你在子类中一不小心重写了个跟父类构造函数一样的函数,那么你的父类的构造函数将被覆盖,也即不能完成父类的构造.就会出错.

在构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。
在析构函数中也不要调用虚函数。在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。
构造函数不能声明为虚函数的原因是:
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。

38. 

当参数*x=1, *y=1, *z=1时,下列不可能是函数add的返回值的( )?
1
2
3
4
5
6
intadd(int*x, int*y, int*z){
    *x += *x;
    *y += *x;
    *z += *y;
    return*z;
 }

正确答案: D   你的答案: 空 (错误)

4
5
6
7
开始不知道啥意思,后经牛客网的大神指点才知道这题要考虑的是,x,y,z三个参数是否指向同一地址(或者说调用该函数时是否实参相同),如:当a=b=c=1时,add(&a,&a,&a),add(&a,&b,&c)。
通过写程序测试得出结果,不可能得到答案7。
39. 

结构体中每一个成员的起始地址要是 该成员大小的整数倍。

40. 

scanf :当遇到回车,空格和tab键会自动在字符串后面添加'\0',但是回车,空格和tab键仍会留在输入的缓冲区中。

gets(): 以回车结束读取,使用'\0'结尾.回车符'\n'被舍弃没有遗留在缓冲区。可以用来输入带空格的字符串。

41. calloc() 申请连续内存并初始化。

42. 友元函数包括3种:设为友元的普通的非成员函数;设为友元的其他类的成员函数;设为友元类中的所有成员函数。

43. const volatile int i = 0; 表示:
任何对i的直接修改都是错误的,但是i可能被意外情况修改掉,不要做无意义的优化。所以,const 和 volatile可以同时出现

44. 静态变量在没有初始化的时候存在BSS段,初始化后存在 data section 段,都在静态存储区。局部静态变量在第一次使用时初始化。

BSS段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
可执行程序包括BSS段、数据段、代码段(也称文本段)。

45. 野指针是指没有初始化的指针,使用野指针(取内容、加减法)都是错误的行为。

46.  C++ 的冒号

表示机构内位域的定义(即该变量占几个bit空间)

typedef struct _XXX{

unsigned char a:4;

unsigned char c;

} ; XXX

47. c++中规定,重载运算符必须和用户定义的自定义类型的对象一起使用。也就是说,不能重载C++默认的运算符,比如double * double

48.  后置加法返回右值,前置加法返回左值

i++:
1
2
3
4
inttemp;
temp = i;
i = i + 1;
returntemp;
++i:
1
2
3
4
inttemp;
temp = i;
i = i + 1;
returni;
49. memcpy 与 memmove的区别

区别:

memcpy和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容,原型分别如下:

[cpp] view plain copy
  1. void *memcpy(void *dst, const void *src, size_t count);  
  2. void *memmove(void *dst, const void *src, size_t count);  

他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。

一、memcpy函数

Memcpy原型:     

[cpp] view plain copy
  1. void *memcpy(void *dest, const void *src, size_t n);  

描述:
        memcpy()函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存区域不能重叠。
返回值:
        memcpy()函数返回指向dest的指针。
二、memmove函数

memmovey原型:

[cpp] view plain copy
  1. void *memmove(void *dest, const void *src, size_t n);  

描述:
       memmove() 函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存可以重叠。
返回值:
        memmove函数返回一个指向dest的指针。

从上面的描述中可以看出两者的唯一区别就是在对待重叠区域的时候,memmove可以正确的完成对应的拷贝,而memcpy不能。

内存覆盖的情形有以下两种,

先看memcpy()和memmove()这两个函数的实现:

[cpp] view plain copy
  1. void* my_memcpy(void* dst, const void* src, size_t n)  
  2. {  
  3.     char *tmp = (char*)dst;  
  4.     char *s_src = (char*)src;  
  5.   
  6.     while(n--) {  
  7.         *tmp++ = *s_src++;  
  8.     }  
  9.     return dst;  
  10. }  

从实现中可以看出memcpy()是从内存左侧一个字节一个字节地将src中的内容拷贝到dest的内存中,这种实现方式导致了对于图中第二种内存重叠情形下,最后两个字节的拷贝值明显不是原先的值了,新的值是变成了src的最开始的2个字节了。

而对于第一种内存覆盖情况,memcpy的这种拷贝方式是可以的。

而memmove就是针对第二种内存覆盖情形,对memcpy进行了改进,改进代码如下:

[cpp] view plain copy
  1. void* my_memmove(void* dst, const void* src, size_t n)  
  2. {  
  3.     char* s_dst;  
  4.     char* s_src;  
  5.     s_dst = (char*)dst;  
  6.     s_src = (char*)src;  
  7.     if(s_dst>s_src && (s_src+n>s_dst)) {      //-------------------------第二种内存覆盖的情形。  
  8.         s_dst = s_dst+n-1;  
  9.         s_src = s_src+n-1;  
  10.         while(n--) {  
  11.             *s_dst-- = *s_src--;  
  12.         }  
  13.     }else {  
  14.         while(n--) {  
  15.             *s_dst++ = *s_src++;  
  16.         }  
  17.     }  
  18.     return dst;  
  19. }  

在第二种内存覆盖的情形下面,memcpy会出错,但是memmove是能正常工作的。


发布了84 篇原创文章 · 获赞 145 · 访问量 9万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章