面试中一些基本概念的辨析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014186096/article/details/49301765

第一问 const与#define相比有什么不同?

答案
1.const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。
2.#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。
3.#define宏没有类型,而const修饰的只读变量具有特定的类型(编译器可以对后者进行类型安全检查,而对前者只进行字符替换,没有安全检查,并且在字符替换中可能会产生意料不到的错误)。
4.有些集成化的调试工具可以对const常量进行调试,但是不能对宏变量进行调试。C++中可以用const定义常量,也可以用#define定义常量,但是基本在C++中只使用const。

第二问 指针和引用的差别?

答:
1.非空区别。在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。
2.合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
3.可修改区别。指针和引用的另一个重要区别就是指针可以重新赋值以指向另一个不同对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
4.应用区别。总的来说,在以下情况下应该使用指针:一是考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空),二是需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。

例题1:

//下面程序中哪里有错?</span>
#include <iostream>
using namespace std;
int main() {
    int iv;                                
    int iv2 = 1024;                        
    int iv3 = 399;                         
    int &reiv;                             
    int &reiv2 = iv;                           
    int &reiv3 = iv;                       
    int *pi;                                   
    *pi = 5;                               
    pi = &iv3;                         
    const double di;
    const double maxWage = 10.0;
    const double minWage = 0.5;
    const double *pc = &maxWage;
 
    cout << pi;
    return 0;
}
错误:7,11,13行。
7:引用必须在声明时同时初始化。
11:pi没有指向,不可以对其赋值。
13: 本地的const常量必须在第一次声明时就初始化。

例题2:

//这个程序有什么问题?该如何修改? 
char *strA() {
    char str[] = “Hello World”;
    return str;
}
Answer:
str[]数组是属于函数strA()的局部变量,当调用完这个函数后,局部变量str被销毁,所以返回的结果是不确定且不安全的。
要获得正确的结果,可做如下修改:
a.  
const char* strA() {
    char *str = “Hello World”;
    //”Hello World”是字符串常量,它位于静态存储区,在程序的生命期内是恒定不变的。
    //str是一个指针变量,它指向这个字符串常量的首地址。
//指针变量str的值是可以改变的(可以改变指向),不能改变的是内存中存储字符串常量的内存单元的值。
    return str;
}


b.
const char* strA() {
    static char str[] = “Hello World”;
/*静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的
函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。*/
    return str;
}

第三问 二维数组与指针

void foo(int[][3]);
int main(void)
{
 int a[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
 foo(a);
 printf("%d\n", a[2][1]);
 return 0;
}
void foo(int b[][3])
{
 ++b;
 b[1][1] = 9;
}
解析:++b的步长实际上是3个int,也就是++b运算以后,b指向{4,5,6}这个数组的开始,而b[1]就是{7,8,9}, b[1][1]实际上就是8这个值也就是main函数中的a[2][1].


第四问 数组指针  

int main()
{
int a[][3] = {1, 2, 3, 4, 5, 6};
int (*ptr)[3] = a;
printf("%d %d ", (*ptr)[1], (*ptr)[2]);
++ptr;
printf("%d %d\n", (*ptr)[1], (*ptr)[2]);
}
解析:依然是2维数组相关题目,ptr为指向int [3]数组的指针,首先指向a[0],所以(*ptr)[1], (*ptr)[2]就是a[0][1], a[0][2].然后++ptr,相当于ptr指向了a[1],这时得到的是a[1][1],a[1][2],所以结果就是2,3, 5, 6。

第五问 在int(*prt)[3];定义中,标识符prt()。

A)定义不合法;
B)是一个指针数组名,每个元素都是一个指向整型变量的指针
C)是一个指针,它指向一个具有三个元素的一位数组
D)是一个指向整型变量的指针


第六问 空指针和迷途指针的区别

#include <iostream.h>
int main(){
 int *pInt = new int;
 *pInt=10;
 cout<<*pInt<<endl;
 delete pInt;
 pInt=0;
 *pInt=20;    // oh no, this was deleled.
 cout<<*pInt<<endl;
 return 0;
}
//Compile this program successfully,but when running it, collapse.

迷途指针也叫悬浮指针,失控指针,是对一个指针delete后---这样会释放它所指向的内存---并没有把它设置为空时产生的.而后,如果你没有赋值就试图再次使用该指针,引起的结果是不可预料的.

当delete一个指针时,实际上仅是让编译器释放内存,但指针本身依然存在。这时它就是一个迷途指针。

当使用以下语句时,可以把迷途指针改为空指针:

MyPtr=0;

通常,如果在删除一个指针后又把它删除了一次,程序就会变得非常不稳定,任何情况都有可能发生。但是如果你只是删除了一个空指针,则什么事都不会发生,这样做非常安全。

使用迷途指针或空指针(如MyPtr=0)是非法的,而且有可能造成程序崩溃。如果指针是空指针,尽管同样是崩溃,但它同迷途指针造成的崩溃相比是一种可预料的崩溃。这样调试起来会方便的多。  

第七问 下面这3个函数哪一个最可能引起指针方面的问题

(a) 只有 f3
(b) 只有f1 and f3
(c) 只有f1 and f2
(d) f1 , f2 ,f3

int *f1(void)
{ 
	int x =10; 
	return(&x);
}
int *f2(void)
{ 
	int*ptr;  
	*ptr =10; 
	return ptr;
}
int *f3(void)
{ 
	int *ptr;  
	ptr=(int*)malloc(sizeof(int)); 
	return ptr;
}
解析:

f1:x是在函数f1中定义的局部变量,生命周期开始于f1函数开始运行,结束于f1函数运行完毕,当f1返回调用它的函数中时,f1就运行完毕了,在f1中定义的变量(如int x)都结束了自己的生命周期,实际上就是存储x的空间被释放了。所以返回指向x的指针时,这个指针指向的内存区域已经被释放了,这个指针也就成了野指针。
f2:定义了int *ptr但是这个指针并没有指向任何内存空间(正确的分配空间的方法可参考f3的分配内存,指针为野指针),就比如现在你只有一个电话号码,而这个号码是空号,你就不能再这个电话号码对应的电话机上做上标记。
f3:函数内申请动态内存空间,函数结束,该内存空间不会释放,函数返回该内存空间地址,外部可以使用,但外部需要手动释放该空间(free(动态内存空间地址))。

第八问 malloc/ free与new/ delete的区别

相同点:

都可用于申请动态内存和释放内存

不同点:

(1)操作对象有所不同。

malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。

(2)用法上也有所不同。

Malloc/free使用要点:

函数malloc 的原型如下
void * malloc(size_t size);
用malloc 申请一块长度为length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
1、malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
2、 malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
函数free 的原型如下
void free( void * memblock );
为什么free 函数不象malloc 函数那样复杂呢?这是因为指针p 的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free
对p 无论操作多少次都不会出问题。如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。

new/delete 的使用要点:

运算符new 使用起来要比函数malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如
Obj *objects = new Obj[100];       // 创建100 个动态对象
不能写成
Obj *objects = new Obj[100](1);        // 创建100 个动态对象的同时赋初值1
在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如
delete []objects; // 正确的用法
delete objects; // 错误的用法
后者相当于delete objects[0],漏掉了另外99 个对象。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        1、new自动计算需要分配的空间,而malloc需要手工计算字节数
        2、new是类型安全的,而malloc不是,比如:
                 int* p = new float[2]; // 编译时指出错误
                 int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
          new operator 由两步构成,分别是 operator new 和 construct
        3、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
        4、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
        5、malloc/free要库文件支持,new/delete则不要。 
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


第九问 realloc/ malloc/ calloc的区别

三个函数的申明分别是: 
void* realloc(void* ptr, unsigned newsize); 
void* malloc(unsigned size); 
void* calloc(size_t numElements, size_t sizeOfElement); 
都在stdlib.h函数库内
它们的返回值都是请求系统分配的地址,如果请求失败就返回NULL 

malloc用于申请一段新的地址,参数size为需要内存空间的长度,如: 
char* p; 
p=(char*)malloc(20);

calloc与malloc相似,参数sizeOfElement为申请地址的单位元素长度,numElements为元素个数,如: 
char* p; 
p=(char*)calloc(20,sizeof(char)); 
这个例子与上一个效果相同

realloc是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度 
如: 
char* p; 
p=(char*)malloc(sizeof(char)*20); 
p=(char*)realloc(p,sizeof(char)*40);

注意,这里的空间长度都是以字节为单位。 
C语言的标准内存分配函数:malloc,calloc,realloc,free等。 
malloc与calloc的区别为1块与n块的区别: 
malloc调用形式为(类型*)malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。 
calloc调用形式为(类型*)calloc(n,size):在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。 
realloc调用形式为(类型*)realloc(*ptr,size):将ptr内存大小增大到size。 


free的调用形式为free(void*ptr):释放ptr所指向的一块内存空间。 
C++中为new/delete函数。

第十问 全局变量、 局部变量、 静态局部变量及静态全局变量的区别

(一)、程序的内存分配

一个由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"优化成一个地方。

}


C++变量根据定义位置的不同,具有不同的作用域,作用域可分为6种:全局作用域,局部作用域,语句作用域,类作用域,命名作用域和文件作用域。

(三)、从作用域看

全局变量具有全局作用域

全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包括全局变量定义的源文件需要用extern关键字再次声明这个全局变量。

静态局部变量具有局部作用域

它只被初始化一次,自从第一次初始化直到程序与你新内阁结束都一直存在,他和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

局部变量也只有局部作用域

他是自动对象,他在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用结束后,变量就被撤销,其所占用的内存也被收回。

静态全局变量也具有全局作用域

他与全局变量的区别在于如果程序包含多个文件的话,他作用于定义它的文件里,不能作用到其他文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同的静态全局变量,他们也是不同的变量。

(四)、从分配内存空间看

全局变量、静态局部变量、静态全局变量都在静态存储区分配空间,而局部变量在栈分配空间

全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上没有什么不同。区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。

1、静态变量会被放在程序的静态数据存储区里,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是他与堆栈变量和堆变量的区别

2、变量用static告知编译器,自己仅仅在变量的作用域范围内可见。这一点是他与全局变量的区别。

从以上分析可以看出,把局部变量改变为静态变量后是改变了他的存储方式,即改变了他的生存期。把全局变量改变为静态变量后是改变了他的作用域,限制了他的使用范围,因此static这个说明符在不同的地方起的作用是不同的。

TIPS:

1、若全局变量仅在单个文件中访问,则可以讲这个变量修改为静态全局变量。

2、若全局变量仅在单个函数中使用,则可以将这个变量修改为该函数的静态局部变量。

3、全局变量、静态局部变量、静态全局变量都存放在静态数据存储区。

4、函数中必须要使用static变量的情况:当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。

第十一问 main/ int main/ void main

main()是省略了返回值类型,C语言会默认认为成main的类型为int,在main()的函数体内要返回一个值,如return 0; void main()的返回值是void类型,也就是说没有返回值。这样在main()的函数体内你就不用写return 0;或者return 1;之类的返回语句。 

在一个程序中,可以说你看不出两者的差别,因为main()是C语言的入口点,入口点如果返回了程序也就结束了,因此C语言的这个特性似乎没用。可没用的话,C语言的设计者为什么要这么做呢?!肯定有用。 

没错,前面我说的是在一个程序中,它似乎没用。可如果一个程序prog2调用另一个程序prog1的话(这里指的是prog2直接调用prog1编译好的可执行文件),那么这个特性就有用了。因为prog2要知道prog1运行之后的状态。比如你用C写了一个删除文件的程序(暂且命名为delf),然后用另一个程序去调用delf,那么调用结果怎么样啊?这个文件是否删掉了?这是int main()就会派上用场了。C语言中默认的main如果返回为0,那么这个程序调用就成功了,其他值,则为不同的错误代码。在你的delf程序中,如果那个文件删除成功,那么就在int main()中返回0,其他调用delf的程序就知道,噢,这个操作成功了。 

在unix/linux系统中的很多命令都是通过这种方式获得运行结果的。 

void main(int argc, char** argv)

 这个函数搞个两星号,看上去比较复杂,但是如果改成这样就感觉熟悉的了许多void main(int argc, char* argv[ ]).

void main(int argc, char* argv[ ]) == void main(int argc, char* *argv)

这行中有三个字符串分别为 cp.exe doc1.txt doc2.txt 

则argc为3,即argc是命令行中参数的个数。

char *argv[]为一个指像字符串的数组。

 

argv[0]="cp.exe"

argv[1]="doc1.txt" 

argv[2]="doc2.txt"

agv[0]为一个字符串指针,它就象 *p=”goodbye”  实际上是*argv[0]=”cp.exe”

p为goodbye的首地址,*p则指向goodbye的第一个字符, *p=”goodbye”与p[ ]=”goodbye”完全致的作用, 同理argv[0]就指向cp.exe的地址,*argv[0]指向cp.exe的第一个字符,即c。

再如下一个文件为test.c的文件

#include <stdio.h>
#include <unistd.h>
#include <string.h>
 
int main(int argc, char **argv)
{   //用 gcc –o a test.c  然后执行 ./a  12345,以下为原程序和执行结果
char *p="goodbye";
char *p1[2]={"hello!!!!!!!","world"};
 
printf("%s\n",p); // goodbye
 
printf("%c\n",*p);//  g
 
printf("%s\n",p1[0]); // hello!!!!!!!
 
printf("%s\n",p1[1]); // world
 
printf("%s\n",argv[0]);//  ./a
 
printf("%s\n",argv[1]);//  12345
 
}
 
 
在你运行程序以后,操作系统会自动将参数传给你。 
例如你编译好的程序叫做program.exe 
你运行 program a b 
这个时候,argc = 3 
argv[0] = "program" 
argv[1] = "a" 
argv[2] = "b"

第十二问 const的用法

const用法主要是防止定义的对象再次被修改,定义对象变量时要初始化变量

下面我就介绍一下几种常见的用法

1.用于定义常量变量,这样这个变量在后面就不可以再被修改

 

const int Val = 10;
 //Val = 20; //错误,不可被修改


2. 保护传参时参数不被修改,如果使用引用传递参数或按地址传递参数给一个函数,在这个函数里这个参数的值若被修改,则函数外部传进来的变量的值也发生改变,若想保护传进来的变量不被修改,可以使用const保护

 

void  fun1(const int &val)
  {
     //val = 10; //出错
}
void fun2(int &val)
{
   val = 10; //没有出错
}
void main()
{
   int a = 2;
   int b = 2;
   fun1(a); //因为出错,这个函数结束时a的值还是2
   fun2(b);//因为没有出错,函数结束时b的值为10
}

如果只想把值传给函数,而且这个不能被修改,则可以使用const保护变量,有人会问为什么不按值传递,按值传递还需要把这个值复制一遍,而引用不需要,使用引用是为了提高效率,如果按值传递的话,没必要加const,那样根本没意义。

 

3. 节约内存空间

#define  PI  3.14 //使用#define宏
 const double Pi = 3.14 //使用const,这时候Pi并没有放入内存中
 double  a = Pi;  //这时候才为Pi分配内存,不过后面再有这样的定义也不会再分配内存
 double  b = PI;  //编译时分配内存
 double  c = Pi;  //不会再分配内存,
 double  d = PI;  //编译时再分配内存

const定义的变量,系统只为它分配一次内存,而使用#define定义的常量宏,能分配好多次,这样const就很节约空间。


4.类中使用const修饰函数防止修改非static类成员变量

 

class
{
 public:
  void fun() const //加const修饰
   {
     a = 10; //出错,不可修改非static变量
     b = 10; //对,可以修改
}
 private:
  int  a ;
  static int b;
}

5.修饰指针

const int *A; 或 int const *A;  //const修饰指向的对象,A可变,A指向的对象不可变
int *const A;              //const修饰指针A, A不可变,A指向的对象可变 
const int *const A;           //指针A和A指向的对象都不可变

6.用const修饰函数的参数

6.1 

如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const 修饰,否则该参数将失去输出功能。

6.2 

const 只能修饰输入参数:

6.2.1如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用

例如StringCopy 函数:

void StringCopy(char *strDestination, const char *strSource);

其中strSource 是输入参数,strDestination 是输出参数。给strSource 加上const修饰后,如果函数体内的语句试图改动strSource 的内容,编译器将指出错误。

6.2.2 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。

例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A 为用户自定义的数据类型。

6.2.3 对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。

为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:

“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。

以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。

问题是如此的缠绵,我只好将“const &”修饰输入参数的用法总结一下。

对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。

 对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。

7.用const 修饰函数的返回值

如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针

例如函数:

const char * GetString(void);

如下语句将出现编译错误:

char *str = GetString();

正确的用法是

const char *str = GetString();

如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。

例如不要把函数int GetInt(void) 写成const int GetInt(void)。

同理不要把函数A GetA(void) 写成const A GetA(void),其中A 为用户自定义的数据类型。

如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。

函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

 

例如:

class A  
{  
  A & operate = (const A &other); // 赋值函数  
 };  
 A a, b, c; // a, b, c 为A 的对象  
   
 a = b = c; // 正常的链式赋值  
 (a = b) = c; // 不正常的链式赋值,但合法 

如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。

8.const 成员函数

                        --------------(const的作用:说明其不会修改数据成员)

任何不会修改数据成员的函数都应该声明为const 类型。如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack 的成员函数GetCount 仅用于计数,从逻辑上讲GetCount 应当为const 函数。编译器将指出GetCount 函数中的错误。

class Stack  
 {  
 public:  
  void Push(int elem);  
  int Pop(void);  
  int GetCount(void) const; // const 成员函数  
 private:  
  int m_num;  
  int m_data[100];  
 };  
 int Stack::GetCount(void) const  
 {  
  ++m_num; // 编译错误,企图修改数据成员m_num  
  Pop(); // 编译错误,企图调用非const 函数  
  return m_num;  
 } 

const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。

关于Const函数的几点规则:

a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.

b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.

c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.

e. 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的。

 对于这样的const function,关键词const到底影响了什么?下面用例子来说明。

class my {  
  public:  
// ...  
  string& operator[](const string& s) const  
  { return table[s]; }  
 private:  
  map table;  
 }; 

上述重载的下标运算函数能否通过编译呢?不能(在mingw32 gcc3.4上)通过。给出了如下的错误提示:

 

passing `const std::map, std::allocator > >'

as `this' argument of `_Tp& std::map<_Key, _Tp, _Compare, _Alloc>::operator[](const _Key&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]' discards qualifiers

 

错误原因就在于,当本重载函数声明为const后,就只能调用其他的同样也声明为const的函数;而table[s]实际上调用的是map的下标重载运算函数,该函数并没有声明为const。所以去掉本函数的const声明,就可以顺利通过编译了。

下面再用一个小例子验证一下吧。

#include  
 #include  
using namespace std;  
class A{  
public:  
 A(int n) { num = n; }  
 void incr() { num += 5; }  
 void disp() const  
 { cout << num ; }  
   
 int times(int m)  
 { return num * m; }  
   
 int f() const  
 {  
 incr();//passing `const A' as `this' argument of `void A::incr()' discards qualifiers  
 disp();// ok  
 times(2);//passing `const A' as `this' argument of `int A::times(int)' discards qualifiers  
 return num;  
 }  
   
 private: int num;  
 };  
   
 int main(int argc, char *argv[])  
 {  
 A a(5);  
 a.f();  
 system("PAUSE");  
 return EXIT_SUCCESS;  
 }  

const 函数只能调用 const 函数,即使某个函数本质上没有修改任何数据,但没有声明为const,也是不能被const函数调用的。

class Test  
{  
 public :  
 int & GetValue()const;  
	 private:  
	 int value;  
};  
 	int &Test::GetValue() const  
 {  
	 return value; //value此时具有const属性,与返回值类型int &的非const属性不匹配  
}  



这样的代码在vs2003中提示的错误:error C2440: “return” : 无法从“const int”转换为“int &”。

在const函数中传递this的时候把this变成了const T* const this(个人理解),所以一个非const的引用指向一个const类型的变量,就会error。

可以这样改,

1.把int value 改成mutable int value. mutable修饰的变量使之在const函数中可以被改变的。

2.return value 改成。 return const_cast(value)。const_cast去掉了const性质。

3.把函数写成const int &Test::GetValue() const ,.这样做的目的是使引用的变量也是const类型的,就相当于const int & b 。

4.把引用去掉,写成返回值类型的。

5.把函数后面的const去掉。

6.返回值不是类的成员变量。

int &Test::GetValue() const  
 {  
 int temp = value;  
 return temp;  
 } 

9.修饰类的成员变量

  使用const修饰的变量必须初始化,在类中又不能在定义时初始化,

如;

class
{
private:
  int a = 10;
  const int b = 10;
  static const int c = 10;
//这样初始化都是错的,
}

 下列关于const关键字的说法错误的是:

A用const常量代替宏定义可以让编译器进行安全性检查

B类的const成员函数不能修改类的成员变量,而且一个const类对象只能调用其const成员函数,不能调用非const成员函数

C  const成员函数与同名、同返回值、同参数列表的非const成员函数属于重载现象

D  推荐使用以下方面定义类成员数组: class A{ … const size_t SIZE=100; int _array[SIZE]; };                                                         【D】

初始化const int类型(没有static),在类的构造函数上初始化

Class Test
{
Public:
  Test():b(23) //构造函数上初始化b的值为23
   {
}
private:
     const int b ;
}
 
初始化static const int这个类型的(带有static的),在类的外面初始化
class Test
{
private:
  static const int c;
} 
const int Test::c=10; //类的外部初始化c为10

10.const和static

成员函数都会在自己的形参表中加一个参数:这个参数是指向函数所在类的类型的CONST指针.比如:

class A
{
 private:
  int n;
 public:
  void set()
  {
    n=10;  
  };
};

上边的set函数,是给n赋值的,但是它怎么知道是给哪一个对象的n赋值呢?这就需要在参数中告诉它。编译器为我们作了这些工作。实际生成的set函数可以理解成这样的:

public A::set(A* const this)

{

  this-〉n=10;

}

而我们调用的时候其实是这样的:

 

A a;

A::set(&a);

 

理解这个对你理解你的这个问题很关键.如果是const成员函数,编译器所作的这项工作也会改变。它会生成一个指向cosnt对象的const指针。所以你不能通过this来改变它所指向的对象。但是要知道static对象并不需要this指针来改变,所以它和const函数是没关系的。

第十三问 逗号运算符   

int main()
{
int a, b, c, d;
a = 3;
b = 5;
c = a, b;
d = (a, b);
printf("c=%d  ", c);
printf("d=%d\n", d);
}
解析:
C 语言中,逗号(,)也可以是运算符,称为逗号运算符(Comma Operator)。逗号运算符可以把两个以上(包含两个)的表达式连接成一个表达式,称为逗号表达式。其一般形式为:

子表达式1, 子表达式2, ..., 子表达式n

例如:

a + b, c = b, c++
逗号运算符的优先级是所有运算符中级别最低的,通常配合 for 循环使用。逗号表达式最右边的子表达式的值即为逗号表达式的值。上例中,c++ 的值(c 自增之前的值)即为该表达式的值。

逗号运算符保证左边的子表达式运算结束后才进行右边的子表达式的运算。也就是说,逗号运算符是一个序列点,其左边所有副作用都结束后,才对其右边的子表达式进行运算。因此,上例中,c 得到 b 的值后,才进行自增运算。

第十四问 sizeof

int main()
{
int i = 3;   int j;
j = sizeof(++i + ++i);
short m;    int n;     double dn;
int t = sizeof ( m + n);
int k = sizeof ( n + n);
int l = sizeof ( m);
int l2 = sizeof (m * m);
int l3 = sizeof (m + dn);
int l4 = sizeof (m + m);
printf("i=%d j=%d t=%d k=%d l=%d l2=%d l3=%d l4=%d\n", i,j,t,k,l,l2,l3,l4);
}

解析:这是求表达式的sizeof,详细参考http://blog.csdn.net/u014186096/article/details/48290013

第十五问 传递

void f1(int*, int);
void (*p[2])(int*, int);
int main(void)
{
 int a = 3;  int b = 5;
 p[0] = f1;
 p[1] = f1;
 p[0](&a, b);
 printf("%d %d ", a, b);
 p[1](&a, b);
 printf("%d %d\n", a, b);
 return 0;
}
void f1(int *p, int q)
{
 int tmp = *p;   *p = q;   q = tmp;
}

解析:详解参考(值传递、指针传递、引用传递)http://blog.csdn.net/u014186096/article/details/48785533

第十六问 CPU进程与线程的关系和区别

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
  线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

进程和线程的关系:

  (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  (3)处理机分给线程,即真正在处理机上运行的是线程。
  (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

进程与线程的区别:

  (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
  (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
  (4) 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些

结论:

  (1)线程是进程的一部分
  (2)CPU调度的是线程
  (3)系统为进程分配资源,不对线程分配资源

线程的执行特性

    线程只有 3 个基本状态:就绪,执行,阻塞。
    线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。

进程通信

    单机系统中进程通信有 4 种形式:主从式,会话式,消息或邮箱机制,共享存储区方式。
    主从式典型例子:终端控制进程和终端进程。
    会话式典型例子:用户进程与磁盘管理进程之间的通信。

几种进程通信方式

# 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
# 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
# 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
# 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
# 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
# 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
# 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

第十七问 链表的意义?链表与数组的区别?

数组定义简单,以连续的变量形式储存,不可以减少或添加任何变量,因此在定义时必须已知长度,可能造成数组不够长或内存浪费的情况;
链表以结构体的自引用为原理,可以在内存中以不连续的方式储存,并动态分配内存,即随时加入或删除一个变量。但链表定义比较复杂,且除头结点外每一个结点都没有名
字,引用起来比较辛苦。如果是已知所需变量数,还是数组方便些。

第十八问 中断

中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准 C 支持中断。具代表事实是,产生了一个新的关键字 
__interrupt。下面的代码就使用了__interrupt 关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。 
__interrupt double compute_area (double radius) 
 double area = PI * radius * radius; 
printf(" Area = %f", area); 
return area; 

}
答:
1). ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。 
2). ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。  
3). 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。 
4). 与第三点一脉相承,printf()经常有重入和性能上的问题。

第十九问 程序崩溃的原因

1、不确定的变量:程序使用了随机的数字或变量组件,在程序执行时没有被很好的确定范围。如:用户输入、随机数或时间。
2、内存泄露:包括堆溢出或栈异常。

第二十问 C/C++的区别

C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。所以C与C++的最大区别在于它们的用于解决问题的思想方法不一样。之所以说C++比C更先进,是因为“ 设计这个概念已经被融入到C++之中 ”,而就语言本身而言,在C中更多的是算法的概念。算法是程序设计的基础,好的设计如果没有好的算法,一样不行。而且,“C加上好的设计”也能写出非常好的东西。 

第二十一问 字符的用法

1.C语言 单引号和双引号的区别

(1)含义不同。
用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。而一般我们的编译器采用的都是ASCII字符集。因此‘s'的含义其实和十进制数115的含义是一致的。
而用双引号引起的字符串,代表的是一个指向无名数组起始字符的指针。
(2)大小不同。
用单引号引起的一个字符大小就是一个字节。
而用双引号引起的字符串大小是字符的总大小+1,因为用双引号引起的字符串会在字符串末尾添加一个二进制为0的字符'\0'。

2.一个字符常量是一个整数

    字符常量书写时将一个字符括在单引号内,字符在机器字符集中的数值就是字符常量的值;某些字符可以通过转义字符序列(例如,换行符\n)表示为字符和字符串常量。


例题1:
printf("abc\tde\b\rf\n");的结果?
\b,只是将光标左移一格,格式化输出行的显示并不清除。使用printf("abc\tde\bf\n")就可以看到效果了:abc    df
没有e了,e被退格了。
例题2:



例题3:
int main()
{
	char a=127;
	int b=4;
	int c;
	c=a+b;
	a+=b;
	printf("%d %d",c,a);
}
解析:a+b把结果转为int型,a转为int型还是127,a+=b;的意思是在char a上加4(char类型范围为-128~127),所以为-125;


第二十二问 extern “C”的作用

简单来说,这个extern“C”用于C++代码调用C的函数。

猜你喜欢

转载自blog.csdn.net/u014186096/article/details/49301765
今日推荐