C/C++关键字篇
语言是编程的基础,掌握基本的语言知识是编程的前提条件。关键字是组成语言的最基本单位,对关键字的理解,有助于编写高质量的代码。
1 static(静态)变量有什么作用?
- 在函数体内,被声明为静态的变量只初始化一次,以后该函数再被调用,将不会再初始化,这就使变量具有“记忆”功能。
- 在模块内(但在函数体外),如果把一个变量或者函数声明为静态的,那么可以将其作用域被限制在本模块内,起一个“隐藏”的作用,避免命名冲突。
- 默认初始化为0,因为静态变量存储在静态数据区,而静态数据区中的所有字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’;太麻烦。如果把字符数组定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’。(全局变量也存储在静态数据区)
- 在C++中,在类中声明static变量或者函数。其初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:
- 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。
- 不能将静态成员函数定义为虚函数。
- 由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X Window系统结合,同时也成功的应用于线程函数身上。
- 静态数据成员是静态存储的,所以必须对它进行初始化。
2 const 有哪些作用?
- 定义常量,使其值不可被修改,另外使编译器可以对其进行类型检查。
- 修饰函数形参,防止值被意外的修改,提高程序的健壮性。
- 修饰常量指针(const char * p)和指针常量(char * const p)。
- 修饰函数返回值,例如给“指针传递”的函数返回值加const,则返回值不能被修改,且该返回值只能被赋值给加const修饰的同类型指针。
- 在C++中,修饰类成员函数,任何不会修改数据成员的函数都应该用const修改。以及修饰类成员数据。
3 volatile在程序设计中有什么作用?
volatile是一个类型修饰符,被其修饰的变量,编译器不会对其进行优化。所以每次用到它的时候都是直接从对应的内存当中提取,而不会利用cache(缓存)或寄存器中的原有数值,以适应它的未知何时会发生的变化。它一般用来修饰多线程间被多个任务共享的变量和并行设备硬件寄存器等。
为什么有些变量要使用volatile取消优化?这是因为,编译器优化的时候可能会出现问题,如当遇到多线程编程时,变量的值可能因为别的线程而改变了,而该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
4 断言ASSERT()是什么?
ASSERT()—般被称为断言,它是一个调试程序时经常使用的宏。它定义在<assert.h>头文 件中,通常用于判断程序中是否出现了非法的数据,在程序运行时它计算括号内的表达式的值。如果表达式的值为false(0),程序报告错误,终止运行,以免导致严重后果,同时也便于查找错误;如果表达式的值不为0,则继续执行后面语句。其用法如下:
ASSERT(n!=0); //分母为0,为非法数据,表达式为false,程序报告错误,程序会终止运行
k=10/n;
ASSERT(n!=0); //分母为0,为非法数据,表达式为false,程序报告错误,程序会终止运行
k=10/n;
需要注意的是,ASSERT()只在Debug版本中有,编译的Release版本则被忽略。还需要注意的一个问题是ASSERT()与assert()的区别,ASSERT()是宏,而assert()是ANSI C标准中规定的函数,它与ASSERT()的功能类似,但是可以应用在Release版本中。
5 char str1[] = "abc"; char str2[] = "abc"; str1 与 str2 不相等,为什么?
两者不相等,是因为str1和str2都是字符数组,每个都有自己的存储区,它们的值都是存储区起始地址。两者不是同一个存储区,所以值肯定不一样。但有些情况不一样,程序示例如下:
#include <iostream>
using namespace std;
int main()
{
const char str3[] = "abc";
const char str4[] = "abc";
const char* str5 = "abc";
const char* str6 = "abc";
cout « boolalpha « ( str3=str4 ) « endl;
cout« boolalpha « ( str5=str6 ) « endl;
return 0;
}
#include <iostream>
using namespace std;
int main()
{
const char str3[] = "abc";
const char str4[] = "abc";
const char* str5 = "abc";
const char* str6 = "abc";
cout « boolalpha « ( str3=str4 ) « endl;
cout« boolalpha « ( str5=str6 ) « endl;
return 0;
}
程序输出为:
fasle
true
fasle
true
str5和str6并非字符数组而是字符指针,并不分配存储区,其后的“abc”以常量形式存于常量区,相同的两个“abc”常量字符串在同一个存储区,而str5和str6是指向该区首地址的指针,所以相等。
6 为什么有时候main()函数会带参数?参数 argc 与 argv 的含义是什么?
C语言的设计原则是把函数作为程序的构成模块。在C99标准中,允许main()函数没有参数,或者有两个参数(有些实现允许更多的参数,但这只是对标准的扩展)。
命令行参数有时用来启动一个程序的执行,如int main(int argc, char *argv[]),其中第一个参数argc表示命令行参数的数目,它是int型的;第二个参数argv是一个指针数组,由于参数的数目并没有内在的限制,所以argv指向这组参数值的第一个元素,这些元素的每个都是指向一个参数文本的指针。
7 C++里面是不是所有的动作都是main()函数引起的?
不是,对于C++程序而言,静态变量、全局变量、全局对象的分配早在main()函数之前己经完成,所以并不是所有的动作都是由main()引起的。在main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的构造及初始化工作。 以如下程序示例代码为例:
class A{};
A a;
int main()
{
...
}
class A{};
A a;
int main()
{
...
}
程序在执行时,因为会首先初始化全局变量,当这个变量是一个对象时,则会首先调用该对象的构造函数,所以上例中,a的构造函数先执行,然后再执行main()函数。所以C++中并非所 有的动作都是main()引起的。
怎样在main()函数退出之后再执行一段代码?答案依然是全局对象,当程序退出时,全局变量必须销毁,自然会调用全局对象的析构函数。
8 *p++与(*p)++等价吗?为什么?
因为优先级顺序的问题,*p++与(*p)++并不等价,前者先完成取值操作,然后对指针地址执行++操作;而后者先完成取值操作,然后对该值进行++运算。
int main(int argc, char* argv[])
{
int a = 0;
int *p1 = &a;
int b = 0;
int *p2 = &b;
*p1++;
(*p2)++;
cout << a << endl;
cout << b << endl;
return 0;
}
/*
0
1
*/
int main(int argc, char* argv[])
{
int a = 0;
int *p1 = &a;
int b = 0;
int *p2 = &b;
*p1++;
(*p2)++;
cout << a << endl;
cout << b << endl;
return 0;
}
/*
0
1
*/
9 前置运算与后置运算有什么区别?
以++操作为例,对于变量a, ++a表示先增加内存中a的值,然后再把值放在装入寄存器中;a++表示先把a的值装入寄存器,然后再增加内存中a的值。
一般而言,当涉及表达式计算时,++a是先将值增加1,再返回其值;而a++是先返回其值,再增加1。
10 a是变量,执行 (a++) += a 语句是否合法?
不合法。a++不能当做左值使用。++a可以当左值使用。a++是先把a的值装入寄存器,然后再增加内存中a的值,此时左值是a的值,而值不能作为左值,所以非法。而++a是先增加内存中a的值,然后再把值放在装入寄存器中,此时左值是a,所以合法。
11 如何进行int、float、bool、指针变量与“零值”的比较?
在编写程序时,经常需要对变量与“零值”进行比较判断。考查对0值判断是衡量程序员基本功的重要标准,不同变量与零值的判断,往往方法也不一样,但很多程序员往往会存在很多误区,将NULL、0、1、FALSE、TRUE的意思混淆。例如,把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变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。示例程序如下:
//int 类型
if (n == 0)
if (n != 0)
//不推荐的写法:因为容易让人误解 n 是布尔类型
if (n)
if (!n)
//float类型
const float EPSINON = 0.00001;
if ((x >= -EPSINON) && (x <= EPSINON))
//错误的写法:因为浮点型变量在内存中的存储导致它并不是一个精确的数
if (x == 0.0)
if (x != 0.0)
//bool类型
if (flag)
if (!flag)
//不推荐的写法:因为多进行了一次判断
if (flag == TRUE)
if (flag == FALSE)
//不推荐的写法:因为容易让人误解 flag 是整型
if (flag == 1)
if (flag == 0)
//指针类型
if (p == NULL)
if (p != NULL)
//不推荐的写法:因为容易让人误解 p 是整型
if (p == 0)
if (p != 0)
//不推荐的写法:因为容易让人误解 p 是布尔类型
if (p)
if (!p)
//int 类型
if (n == 0)
if (n != 0)
//不推荐的写法:因为容易让人误解 n 是布尔类型
if (n)
if (!n)
//float类型
const float EPSINON = 0.00001;
if ((x >= -EPSINON) && (x <= EPSINON))
//错误的写法:因为浮点型变量在内存中的存储导致它并不是一个精确的数
if (x == 0.0)
if (x != 0.0)
//bool类型
if (flag)
if (!flag)
//不推荐的写法:因为多进行了一次判断
if (flag == TRUE)
if (flag == FALSE)
//不推荐的写法:因为容易让人误解 flag 是整型
if (flag == 1)
if (flag == 0)
//指针类型
if (p == NULL)
if (p != NULL)
//不推荐的写法:因为容易让人误解 p 是整型
if (p == 0)
if (p != 0)
//不推荐的写法:因为容易让人误解 p 是布尔类型
if (p)
if (!p)
注意“指针类型”的第二种误用情况,为了偷懒或者节省一些效率,经常犯。
12 new/delete与malloc/free的区别是什么?
- new/delete是操作符,而malloc/free是函数,在C语言中需要<stdlib.h>的支持。
- new能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。例如,int* p1 = new int[2],int* p2 = malloc(2*sizeof(int))。
- new与delete直接带具体类型的指针,而malloc与free返回void类型的指针。
- new是类型安全的,而malloc不是,例如,int* p = new float[2],编译时就会报错; 而int* p = malloc(2*sizeof(float)),编译时编译器就无法指出错误来。
- new将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。
delete或free仅仅是告诉操作系统,这一块内存被释放可以用做其他用途。但是,由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,会出现野指针的情况。因此,释放完内存后,应该将指针指向空。程序示例如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void TestFree()
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str!=NULL)
{
strcpy(str, "world");
printf("%s\n",str);
}
}
int main()
{
TestFree();
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void TestFree()
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str!=NULL)
{
strcpy(str, "world");
printf("%s\n",str);
}
}
int main()
{
TestFree();
return 0;
}
程序输出为
world
world
通过上例可知,free或delete调用后,内存其实并没有释放,也没有为空,而是还存储有内容,所以在将资源free或delete调用后,还需要将其置为NULL才行。
13 C语言中,整型变量x小于0,是否可知x*2也小于0?
假定计算机是32位的,用2的补码表示整数,若x<0,则x*2<0不一定成立。例如,当x为整型值的最小值时就不成立。程序示例代码如下:
#include <stdio.h>
int main()
{
int x = -4292967295;
if(x*2<0)
printf("2*x<0");
else
printf("2*x>0");
}
#include <stdio.h>
int main()
{
int x = -4292967295;
if(x*2<0)
printf("2*x<0");
else
printf("2*x>0");
}
程序输出为:
2*x>0
2*x>0
1.13 已知string类定义,如何实现其函数体?
string类定义如下:
class string
{
public:
string(const char *str = NULL); //通用构造函数
string(const string &another); //复制构造函数
~string(); //析构函数
string &operator = string(const string &rhs); //赋值函数
private:
char *m_data; //用以保存字符串
};
class string
{
public:
string(const char *str = NULL); //通用构造函数
string(const string &another); //复制构造函数
~string(); //析构函数
string &operator = string(const string &rhs); //赋值函数
private:
char *m_data; //用以保存字符串
};
在这个类中包括了指针类成员变量m_data,所以需要自定义其复制构造函数、赋值运算操作符函数,避免单纯的指针值的复制。具体而言,String类的函数体实现代码如下:
#include <iostream>
#include <string.h>
using namespace std;
string::string(const char *str)
{
if(str==NULL) //构造函数参数为默认参数NULL的情况,例如:string str;
{
m_data = new char[1];
strcpy(m_data, '\0');
}
else
{
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
}
string::string(const string &another)
{
m_data = new char[strlen(another.m_data)+1];
strcpy(m_data, another.m_data);
}
String::〜String()
{
delete[] m_data;
m_data = NULL;
}
String& String::operator =(const String &rhs)
{
if(this = &rhs) //避免出现:str = str;
return *this;
delete []m_data; //避免出现:str1="123"; str2="abcde"; str1=str2;
m_data = new char[strlen(rhs.m_data)+l];
strcpy (m_data,rhs. mdata);
return *this;
}
int main()
{
String a("abcdefg");
printf("%s\n",a);
String b(a);
printf("%s\n",b);
String c=b;
printf("%s\n",c);
return 0;
}
#include <iostream>
#include <string.h>
using namespace std;
string::string(const char *str)
{
if(str==NULL) //构造函数参数为默认参数NULL的情况,例如:string str;
{
m_data = new char[1];
strcpy(m_data, '\0');
}
else
{
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
}
string::string(const string &another)
{
m_data = new char[strlen(another.m_data)+1];
strcpy(m_data, another.m_data);
}
String::〜String()
{
delete[] m_data;
m_data = NULL;
}
String& String::operator =(const String &rhs)
{
if(this = &rhs) //避免出现:str = str;
return *this;
delete []m_data; //避免出现:str1="123"; str2="abcde"; str1=str2;
m_data = new char[strlen(rhs.m_data)+l];
strcpy (m_data,rhs. mdata);
return *this;
}
int main()
{
String a("abcdefg");
printf("%s\n",a);
String b(a);
printf("%s\n",b);
String c=b;
printf("%s\n",c);
return 0;
}
程序输出为:
abcdefg
abcdefg
abcdefg
abcdefg
abcdefg
abcdefg
14 在C++中如何实现模板函数的外部调用?
export是C++新增的关键字,它的作用是实现模板函数的外部调用,类似于extern关键字。为了访问其他代码文件中的变量或对象,对普通类型(包括基本数据类、结构和类)可以利用关键字extern来使用这些变量或对象,但对于模板类型,则可以在头文件中声明模板类和模板函数,在代码文件中使用关键字export来定义具体的模板类对象和模板函数,然后在其他用户代码文件中,包含声明头文件后,就可以使用这些对象和函数了。使用方法如下:
export template <class T> class Stack<int> s;
export template<class T> void f(T&t){...}
export template <class T> class Stack<int> s;
export template<class T> void f(T&t){...}
15 在C++中,关键字explicit有什么作用?
在C++中,如下声明是合法的:
class String
{
String(const char* p);
};
String s1 = "hello";
class String
{
String(const char* p);
};
String s1 = "hello";
上例中,String s1 = "hello"
会执行隐式转换,等价于String s1 = String("hello")
。为了避免这种情况的发生,C++引入了关键字explicit,它可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。
16 C++中异常的处理方法以及使用了哪些关键字?
C++异常处理使用的关键字有:try、catch、throw。
- try - 标识可能出现的异常代码段
- throw - 抛出一个异常(throw必须在try代码块中,后边跟的值决定抛出异常的类型)
- catch - 标识处理异常的代码段
C++中的异常处理机制只能处理由throw捕获的异常,没有捕获的将被忽略。使用try{ } catch() { }句来捕获异常,把可能发生异常的代码放在try{ }语句块中,后面跟若干个Catch() { }语句负责处理具体的异常类型,这样一组有try块和不少于一个的catch块就构成了一级异常捕获。如果本级没有带适当类型参数的catch块,将不能捕获异常,异常就会向上一级传递,函数调用处如果没有捕获住异常,则直接跳到更高一层的调用者,如果一直没有捕获该异常,C++会使用默认的异常处理函数,该函数可能会让程序最终跳出main()函数并导致程序异常终止。看下面这个JAVA示例程序(标准C++没有finally语句块,但微软做了扩展):
public class Test
{
public static int testFinally()
{
try
{
return 1;
}
catch (Exception e)
{
return 0;
}
finally
{
System.out.println("execute finally");
}
}
public static void main(String[] args)
{
int result = testFinally();
System.out.println(result);
}
}
public class Test
{
public static int testFinally()
{
try
{
return 1;
}
catch (Exception e)
{
return 0;
}
finally
{
System.out.println("execute finally");
}
}
public static void main(String[] args)
{
int result = testFinally();
System.out.println(result);
}
}
运行结果:
execute finally
1
execute finally
1
由于程序执行return就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在return前执行(除非碰到exit函数),因此finally块里的代码也是在return之前执行的。
17 如何定义和实现一个类的成员函数为回调函数?
回调函数就是被调用者回头调用的函数,它是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,此时就可以称它为回调函数。回调函数不是由该函数的实现方直接调用的,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数 (这个函数为回调函数)的地址作为参数传递给那个被调用函数。而该被调用函数在需要的时候,利用传递的地址调用回调函数。
回调函数由程序员自己编写,当需要调用另外一个函数时,这个函数的其中一个参数就是这个回调函数名。系统在必要的时候就会调用程序员写的回调函数,这样就可以在回调函数里完成要做的事。要定义和实现一个类的成员函数为回调函数需要做3件事:
- 声明。
- 定义。
- 设置触发条件,就是在函数中把回调函数名作为一个参数,以便系统调用。
声明回调函数类型示例如下:
typedef void (*funPtr) ();
//定义会掉函数
class A
{
public:
//回调函数,必须声明为static
static void callbackFun()
{
cout << "callback!" << endl;
}
};
//设置触发条件
void funtype(funPtr p)
{
p();
}
void main()
{
funtype(A::callbackFun);
}
/*
输出:
callback!
*/
typedef void (*funPtr) ();
//定义会掉函数
class A
{
public:
//回调函数,必须声明为static
static void callbackFun()
{
cout << "callback!" << endl;
}
};
//设置触发条件
void funtype(funPtr p)
{
p();
}
void main()
{
funtype(A::callbackFun);
}
/*
输出:
callback!
*/
回调函数与应用程序接口(API)非常接近,它们都是跨层调用的函数,但区别是API是低层提供给高层的调用,一般这个函数对高层都是已知的。而回调函数正好相反,它是高层提供给底层的调用,对于低层它是未知的,必须由高层进行安装,这个安装函数其实就是一个低层提供的API,安装后低层不知道这个回调的名字,但它通过一个函数指针来保存这个回调函 数,在需要调用时,只需引用这个函数指针和相关的参数指针。