程序设计基础-C/C++关键字(程序员面试笔试宝典)

简介

生活中的点点滴滴离不开计算机程序,没有程序则没有现代化的生活。coding已不是理工科学生的专属了,任何学科的学生只要掌握一定的编程基础知识,通过系统联系,便可以掌握IT研发工作,成为一名优秀的程序员,本内容详细介绍C/C++当中的关键字。

static(静态)变量有什么作用

在C语言编程领域中,关键字static扮演着三种关键角色,深刻影响着变量和函数的生命周期、作用域及其访问特性:

  1. 函数内部的静态变量:赋予了“持久性”特征。这意味着,一旦在函数内部声明一个变量为static,该变量就能在函数多次调用的过程中保留其值,不会因函数退出而重置,实现了跨调用的记忆功能。

  2. 模块级别的静态声明:不论是变量还是函数,当在函数外部标记为static时,它们的作用域就被限定在声明它们的源文件内部。这样的变量成为了局部全局变量,能被同一文件中的所有函数共享,却对外部世界隐藏,有效避免了命名冲突并增强了代码的封装性。静态函数同样受限于声明它的文件,仅允许内部调用,增强了模块的独立性。

  3. 关于函数的定义与声明规范:推荐实践指出,打算在多个源文件间共享的函数应在头文件中声明,而实际的函数定义则应放置在单独的源文件中,需要使用这些函数的文件需包含相应的头文件。至于那些仅服务于定义它们的文件的辅助函数,则宜声明为static,以保持文件内的私密性和减少对外接口的复杂度。

进一步细化,static全局变量与常规全局变量的主要区别,在于前者仅在程序启动时初始化一次,此机制确保了该变量不被外部翻译单元无意间修改或访问,增强了数据的安全性。同样地,static局部变量虽位于函数内部,但其生命周期拓展至整个程序执行期间,并保持其值跨函数调用,与每次函数调用时重新初始化的普通局部变量形成对比。

在C++编程语言中,为类的数据成员附加static关键字,会将其定义为类的静态数据成员,具备以下显著特征:

  1. 单一实例与共享访问:与每个类实例拥有独立副本的非静态数据成员不同,静态数据成员在程序中仅为该类维护一份公共拷贝。不论创建了多少该类的对象,所有实例均共享此静态成员,促进了资源的有效利用和数据同步。

  2. 存储位置与生命周期:静态数据成员驻留于全局数据区域,其存储空间在编译阶段即被分配,不允许在类声明内部直接初始化。由于其生命周期与整个程序的运行期相同,即使未实例化任何类对象,静态数据成员也可被访问和操作。

  3. 访问控制机制:静态数据成员遵循与普通数据成员相同的访问权限(public, protected, private),确保了数据的安全性和类设计的灵活性。尽管可以在类外初始化,但访问权限规则仍然适用,保护了private或protected成员不被外界直接访问。

  4. 初始化与限制:静态成员变量要求显式在类定义外部进行初始化,此时初始化语句不再需要static关键字。值得注意的是,静态成员的初始化发生在程序启动阶段,早于main()函数的执行,并且如果静态成员是类类型,则会触发该类的构造函数。

与全局变量相比较,采用静态数据成员的优势体现在:

  • 避免命名冲突:静态数据成员属于类的命名空间,避免了与全局变量的命名冲突,提升了程序的可维护性。
  • 增强封装性:静态数据成员能够声明为private,提供了一种机制来隐藏实现细节,这是全局变量所不具备的特性。

此外,静态成员函数作为类的一部分,不依赖于特定对象实例,不携带this指针,故无法直接访问非静态成员或调用非静态成员函数,仅能操作其他静态成员。这种设计使静态成员函数能够执行与类相关的操作,而不受特定对象状态的影响,进一步支持了类的逻辑分离和通用功能的实现。

引申1:为什么static变量只初始化一次?

对于所有的对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有“记忆”功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。

存放在静态区的变量的生命周期一般比较长,一般与整个源程序“同生死、共存亡”,所以它只需初始化一次。而auto变量,即自动变量,由于存放在栈区,一旦调用过程结束,就会立刻被销毁。

分析以下程序代码:

#include <stdio.h>

void fun(int a)
{
    static int a_value = a++;
    printf("%d\n",a_value);
}

int main()
{
    fun(0);
    fun(1);
    fun(2);
    return 0;
}

 程序输出为

0
0
0

程序每次输出都为0,是因为a_value是静态类型(static),只会定义一次。也就是说,不管调用fun()这个函数多少次,static int a_value=a++这个定义语句只会在第一次调用的时候执行,由于第一次执行的时候a=0,所以a_value也就被初始化成0了,以后调用fun()都不会再执行这条语句的。

分析以下一段代码:

#include <stdio.h>

void fun(int a)
{
    static int a_value = a++;
    a_value =  a++;
    printf("%d\n",a_value);
}

int main()
{
    fun(0);
    fun(1);
    fun(2);
    return 0;
}

程序输出为

1
1
2

上述代码之所以输出为1,1,2,是因为当调用fun(0)时,由于a_value被声明为static,所以定义语句只执行一次,此时a_value=a++,a_value的值为0,i的值变为1,执行第二行语句a_value=a++后,此时a_value的值为i的初值为1,接着i的值变为2,所以第一次输出为1。当调用fun(1)时,因为a_value是静态变量,具有记忆功能,所以会跳过定义语句,只执行a_value=a++语句,所以a_value的值为1,而此时a的值变为2,所以第二次调用时输出为1。当调用fun(2)的时候,也会跳过定义语句,只执行a_value=a++语句,所以a_value的值为2,a的值变为3,所以第三次调用时输出为2。

引申2:在头文件中定义静态变量,是否可行?为什么?

不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误。

所以不推荐在头文件中定义任何变量,当然也包括静态变量。

const有哪些作用

在C及C++编程语言中,const关键字是一个重要的类型修饰符,用于标示常量类型。它不仅能够用来声明不可修改的变量或对象,还在函数的参数、返回值乃至函数定义本身(尤其是C++中包括类的成员函数)中发挥着控制不变性的关键作用。简而言之,const的运用确保了被其修饰的元素在程序执行过程中其值保持恒定,不允许被程序逻辑直接改变,从而增强了程序的安全性、可读性和效率。

一般而言,const有以下几个方面的作用:

(1)定义const常量,具有不可变性。例如:

const int MAX = 100;
int Array[MAX];

(2)进行类型检查,使编译器对处理内容有更多的了解,消除了一些隐患。例如:void f(const int i){...}编译器就会知道 i 是一个常量,不允许修改。

(3)避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。同宏定义一样,可以做到不变则已,一变都变。如(1)中,如果想修改MAX的内容,只需要定义const int MAX=期望值即可。

(4)保护被修饰的东西,防止被意外的修改,增强了程序的健壮性。在上例中,如果在函数体内修改了变量i的值,那么编译器就会报错。例如:

void f(const int i)
{
    i = 10;
}

上述代码对i赋值会导致编译错误。

(5)为函数重载提供参考。

class A
{
    void f(int i){...}        // 定义一个函数
    void f(const int i){...}  // 上一个函数的重载
}

(6)节省空间,避免不必要的内存分配。例如:

#define PI 3.14159          //该宏用来定义常量
const doulbe Pi=3.14159;    //此时并未将Pi放入只读存储器中
double i=Pi;                //此时为Pi分配内存,以后不再分配
double I=PI;                //编译期间进行宏替换,分配内存
double j=Pi;                //没有内存分配
double J=PI;                //再次进行宏替换,又一次分配内存

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以const定义的常量在程序运行过程中只有一份复制品,而#define定义的常量在内存中有若干个复制品。

(7)提高了程序的效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

引申1:什么情况下需要使用const关键字?

(1)修饰一般常量。一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如:int const x=2或const int x=2。

(2)修饰常数组。定义或说明一个常数组可以采用如下格式:

int const a[8]={1,2,3,4,5,6,7,8};
const int a[8]={1,2,3,4,5,6,7,8};

(3)修饰常对象。常对象是指对象常量,定义格式如下:

class A;
const A a;
A const a;

定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。

(4)修饰常指针。

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

(5)修饰常引用。使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。其定义格式如下:

const double &v;

(6)修饰函数的常参数。const修饰符也可以修饰函数的传递参数,格式如下:

void Fun(const int Var);

(7)修饰函数的返回值。const修饰符也可以修饰函数的返回值,返回值不可被改变,格式如下:

const int Fun1( ); 
const MyClass Fun2( );

(8)修饰类的成员函数。const修饰符也可以修饰类的成员函数,格式如下:

class ClassName
{
    public:
    int Fun( ) const;
};

(9)在另一连接文件中引用const常量。使用方式有:

extern const int i;
extern const int j = 10;  // 此处是引用,并非初始化

第一种用法是正确的,而第二种用法是错误的,常量不可以被再次赋值。另外,还要注意,常量必须初始化,如const int i=5。

引申2:什么是常引用?

常引用也称为const引用。之所以引入常引用,是为了避免在使用变量的引用时,在毫不知情的情况下改变了变量的值,从而引起程序错误。常引用主要用于定义一个普通变量的只读属性的别名,作为函数的传入形参,避免实参在调用函数中被意外地改变。

const引用的意思是指向const对象的引用,非const引用表示指向非const类型的引用。如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:

const 类型标识符 & 引用名 = 目标变量名;

常引用的主要用途如下:

(1)用做普通变量的只读属性的别名。通常这个别名只能获得这个变量的值,而不能改变这个变量的值。

(2)用于函数的形参。常引用做形参,可以确保在函数内不会改变实参的值,所以参数传递时尽量使用常引用类型。

如果是对一个常量进行引用,则编译器首先建立一个临时变量,然后将该变量的值置入临时变量中,对该引用的操作就是对该临时变量的操作,对常量的引用可以用其他任何引用来初始化,但不能改变。

关于引用的初始化,一般需要注意以下问题:当初始化值是一个左值(可以取得地址)时,没有任何问题;而当初始化值不是一个左值时,则只能对一个常引用赋值,而且这个赋值是有一个过程的,首先将值隐式转换到类型T,然后将这个转换结果存放在一个临时对象里,最后用这个临时对象来初始化这个引用变量。例如如下两种使用方式:

(1) double&dr=1;
(2) const double&cdr=1;

第(1)种方法错误,初始化值不是左值,而第(2)种方法正确,其对应的执行过程如下:

double temp = double(1);
const double& cdr = temp;

如果对第(1)种使用方法进行相应的改造,也可以变为合法,例如:

const int ival = 1024;

(1)const int&refVal=ival;

(2)int&ref2=ival;

在上例中,第(1)种方法的引用是合法的,而第(2)种方法的引用是非法的。上例中,可以读取refVal的值,但是不能修改它,因为refVal的类型是const,任何对refVal的赋值都是不合法的(const引用是只读的,常量即不能作为左值的量,定义式中赋初值除外)。同时,const引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量,而非const引用只能绑定到与该引用同类型的对象。例如,下述const引用都是合法的。

int i= 42;
const int &r= 42;
const int &r2 = r+ i;

在使用const引用进行函数调用的时候,需要注意一个问题,例如如下函数声明:

void bar(string & s);

那么下面的表达式将是非法的:

bar("hello world");

程序示例如下所示:

#include <iostream>
#include <string>
using namespace std;
void bar(string& s)
{
    cout<<s<<endl;
}
int main()
{
    bar("hello world");
    return 0;
}

程序输出为

hello world

原因在于“hello world”串会产生一个临时对象,而在C++中,临时对象是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const。

switch,case,break语句,关系是怎样的呢

switch语句中的case结尾是否必须添加break语句?为什么

在switch语句中,每个case分支后的break语句并非强制性要求,但通常建议添加。其原因在于break语句起到了“跳出”switch结构的作用。当程序执行到某个case匹配成功后,如果不跟随一个break语句,控制流会继续“贯穿”到下一个case,执行后续所有case中的代码,直到遇到break或者switch语句自然结束。这被称为“fallthrough”现象。程序代码如下所示:

#include <stdio.h>
int main()
{
    int i;
    for(i=0;i<3;i++)
    {
        switch(i)
        {
            case 0:
                printf("%d\n",i);
            case 2:
                printf("%d\n",i);
            default:
                printf("%d\n",i);
        }
    }
    return 0;
}

输出为:

0
0
0
1
2
2

case 0的时候执行3次打印,case 1的时候执行一次defalut,case 2的时候执行两次打印。如果将case 2后面添加break语句,则最后输出为0012,因为此时case 0执行两次,case1执行一次defalut,case 2执行一次。

需要注意的是,switch(c)语句中c可以是int、long、char、unsigned int等类型,唯独不可以是float类型。

 volatile在程序设计中有什么作用

在多线程编程环境下,编译器的优化措施可能引发问题,尤其是当变量的状态可能被其他线程动态改变时。具体而言,为提升执行效率,编译器有时会将频繁访问的变量缓存至寄存器中,首次读取后,后续对该变量的访问直接从寄存器获取,而非每次都从内存中读取。然而,如果另一个线程在此期间修改了该变量的值,寄存器中的值未能及时反映这一变化,导致当前线程看到的是过时的、不一致的变量值,这就是所谓的“缓存一致性”问题。

为解决这一问题,C/C++语言引入了volatile关键字作为类型修饰符。volatile的作用在于指示编译器不对该修饰的变量进行任何优化假设,每次访问该变量时,编译器都会确保直接从主内存中读取其最新值,同样,对该变量的修改也会立即写回内存,确保所有线程对这个变量的观察都是最新的、一致的状态。这对于那些可能被外部事件(如硬件中断、其他线程活动)影响的变量,或是用于内存映射的硬件寄存器等场景尤为重要。

验证volatile关键字的效果,可以通过对比加入与不加入volatile修饰时,编译生成的汇编代码差异来进行直观理解。这种方式可以帮助开发者深入理解volatile如何影响编译器的代码生成策略,以及它是如何确保程序在并发环境下的正确执行的。

首先建立一个voltest.cpp文件,输入下面的代码:

#include <stdio.h>
int main()
{
    int i = 10;
    int a = i;
    printf("i= %d\n",a);//下面汇编语句的作用是改变内存中i的值,但是又不让编译器知道
    _asm     // 用于调用内联汇编程序
    {
        mov dword ptr [ebp-4],20h     // 此处是汇编指令,移动相关地址中的数据
    }
    int b= i;
    printf("i= %d\n",b);
    return 0;
}

在debug调试版本模式运行程序,输出结果如下:

i= 10
i= 32

在release版本模式运行程序,输出结果如下:

i= 10
i= 10

输出的结果明显表明,在release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。把i的声明加上volatile关键字,程序实例如下:

#include <stdio.h>
int main()
{
    volatile int i = 10;
    int a= i;
    printf("i= %d\n",a);//下面汇编语句的作用是改变内存中i的值,但是又不让编译器知道
    _asm     // 用于调用内联汇编程序
    {
        mov dword ptr [ebp-4],20h     // 此处是汇编指令,移动相关地址中的数据
    }
    int b= i;
    printf("i= %d\n",b);
    return 0;
}

分别在debug调试版本和release发布版本运行程序,输出如下所示:

i= 10
i= 32

一个定义为volatile的变量是说这个变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了。准确地说,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值(From Memory),而不是使用保存在寄存器里的备份。

断言ASSERT()是什么

ASSERT()一般被称为断言,它是一个调试程序时经常使用的宏。它定义在<assert.h>头文件中,通常用于判断程序中是否出现了非法的数据,在程序运行时它计算括号内的表达式的值。如果表达式的值为false(0),程序报告错误,终止运行,以免导致严重后果,同时也便于查找错误;如果表达式的值不为0,则继续执行后面语句。在此需要强调一点,ASSERT()捕获的是非法情况,而非错误情况,错误情况是必然存在的,并且一定需要作出相应的处理,而非法情况则不是,它可能只是漏洞而已。

其用法如下:

ASSERT(n!=0);
k=10/n;

需要注意的是,ASSERT()只在Debug版本中有,编译的Release版本则被忽略。还需要注意的一个问题是ASSERT()与assert()的区别,ASSERT()是宏,而assert()是ANSIC标准中规定的函数,它与ASSERT()的功能类似,但是可以应用在Release版本中。

使用assert()的缺点是,频繁的调用会极大地影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include<assert.h>的语句之前插入#define NDEBUG来禁用assert()调用,示例代码如下:

#include <stdio.h>
#define NDEBUG
#include <assert.h>

对于assert()的使用,需要注意以下几个方面:

(1)在函数开始处检验传入参数的合法性。例如:

assert(nNewSize >= 0);
assert(nNewSize <= MAX BUFFER SIZE);

(2)每个assert()一般只检验一个条件,而不对多个条件进行检验,因为同时检验多个条件时,如果断言失败,则无法直观地判断是哪个条件失败。例如,assert(nOffset>=0&&nOffset+nSize<=m_nInfomationSize)就不是一种高效的方式,它无法判断是nOffset>=0有误还是nOffset+nSize<=m_nInfomationSize有误,而将该语句分开表示为如下两个简单语句则更好:assert(nOffset>=0)和assert(nOffset+nSize<=m_nInfomationSize)。

(3)不能使用改变环境的语句,因为assert只在DEBUG时生效,如果这么做,会使程序在真正运行时遇到问题。例如,assert(i++<100)就是错误的。如果执行出错,在执行之前i=100,那么这条语句就不会执行,i++这条命令就没有执行。而正确的写法应该为assert(i<100);i++。

(4)并非所有的assert()都能代替过滤条件,对于有的地方,assert()无法达到条件过滤的目的。

(5)一般在编程的时候,为了形成逻辑和视觉上的一致性,会将assert()与后面的语句之间空一行来隔开。

枚举变量的值如何计算

以如下程序实例进行分析。

#include <stdio.h>
int main()
{
    enum{a,b=5,c,d=4,e};
    printf("%d %d %d %d %d\n",a,b,c,d,e);
    return 0;
}

程序输出为

0 5 6 4 5

为什么c的值为6呢?其实,在枚举中,某个枚举变量的值默认为前一个变量的值加1,而如果第一个枚举变量没有被赋值,则其默认值为0。所以在上例中,a,b,c,d,e的值分别为0,5,6,4,5,其中b与e的值都为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;
}

程序输出为

false
true

为什么上面程序示例的输出结果不都是false呢?上例中,str3和str4两个字符数组都存储在栈空间上,但两者地址值不相等。而str5和str6并非字符数组而是字符指针,并不分配存储区,其后的“abc”以常量形式存于常量区,str5和str6是指它们指向的地址的首地址,而它们自己仅是指向该区首地址的指针,所以相等(&str5和&str6是指指针自己的地址,所以两者地址是不相等的)。

 为什么有时候main()函数会带参数?参数argc与argv的含义是什么

C语言的设计原则是把函数作为程序的构成模块。在C99标准中,允许main()函数没有参数,或者有两个参数(有些实现允许更多的参数,但这只是对标准的扩展)。

命令行参数有时用来启动一个程序的执行,如int main(int argc,char*argv[]),其中第一个参数argc表示命令行参数的数目,它是int型的;第二个参数argv是一个指向字符串的指针数组,由于参数的数目并没有内在的限制,所以argv指向这组参数值(从本质上说是一个数组)的第一个元素,这些元素的每个都是指向一个参数文本的指针。

程序代码(程序名为a.c)如下:

#include <stdio.h>
int main(int argc, char *argv[])
{
    int count;
    printf("该命令一共有%d个参数:\n",argc-1);
    for(count=1;count<argc;count++)
        printf("%d: %s\n",count,argv[count]);
    return 0;
}

编译运行,在命令行输入c I love you回车,下面是运行该程序的结果:

该命令一共有3个参数:

1:I
2:love
3:you

在本例中,程序从命令行中接受了4个字符串(此处包括程序名),并将它们存储在字符串数组中,其中argv[0]表示a(程序名),argv[1]对应字符串I,argv[2]对应字符串love,argv[3]对应字符串you。argc的值为参数的个数,程序自动统计。

同时需要注意的是,一个C语言程序总是从main()函数开始执行的。

 C++里面是不是所有的动作都是main()函数引起的

不是,对于C++程序而言,静态变量、全局变量、全局对象的分配早在main()函数之前已经完成。所以并不是所有的动作都是由main()引起的,只是编译器是由main()开始执行的,main()只不过是一个约定的函数入口,在main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的构造及初始化工作。

以如下程序示例代码为例:

class A{};
A a;
int main()
{
    ...
}

程序在执行时,因为会首先初始化全局变量,当这个变量是一个对象时,则会首先调用该对象的构造函数,所以上例中,a的构造函数先执行,然后再执行main()函数。C++中并非所有的动作都是main()引起的。

怎样在main()函数退出之后再执行一段代码?答案依然是全局对象,当程序退出时,全局变量必须销毁,自然会调用全局对象的析构函数,所以剩下的就同构造函数一样了。

*p++与(*p)++等价吗?为什么

在回答这个问题前,必须弄明白一个问题,就是C语言中操作符的优先级问题。在C语言中,优先级由高到低的排序主要遵循如下规则:

(1)函数符号(),数组下标[],数组下标符号.,成员符号->,结合性从左往右。

(2)单目运算符:!,~,++,--,-(type)*,&,sizeof结合性从右往左。

(3)算术运算法:*、/、%,结合性从左往右。

(4)+、-结合性从左往右。

(5)移位运算符:<<,>>,>>>结合性从左往右。

(6)关系运算符:<,<=,>,>=结合性从左往右。

(7)==,!=结合性从左往右。

(8)逻辑运算符:首先,按位运算符&、^与|,且&高于^,^高于|,结合性从左往右;其次,逻辑运算符&&与‖,且&&高于‖,结合性从左往右。

(9)三目运算符?:,结合性从右往左;其次是赋值运算符=,结合性从右往左;最后是逗号运算符,结合性从左往右。

对于操作符的优先级总结如下:

(1)关系运算符优于逻辑运算符。

(2)移位运算符介于算术运算符和比较运算符之间。

(3)除单目运算符外,算术运算符的优先级最高。

所以,因为优先级顺序的问题,*p++与(*p)++并不等价,前者先完成取值操作,然后对指针地址执行++操作;而后者为首先执行取值操作,然后对该值进行++运算。

前置运算与后置运算有什么区别

以++操作为例,对于变量a,++a表示取a的地址,增加它的内容,然后把值放在寄存器中;a++表示取a的地址,把它的值装入寄存器,然后增加内存中a的值。

一般而言,当涉及表达式计算时,对这两种情况的计算过程区分如下:后置的++运算符是先将其值返回,然后其值增1;而前置的++运算符,则是先将值增1,再返回其值。程序实例如下:

#include<stdio.h>
int main()
{
    int a,b,c,d;
    a=10;
    b=a++;
    c=++a;
    d=10*a++;
    printf("%d\n%d\n%d\n%d\n" ,a,b,c,d);
    return 0;
}

程序输出为

13
10
12
120

上例中,首先赋值a为10,然后执行b=a++语句,由于后置操作符的特性,所以首先执行b=a操作,及b的值为10,然后执行a的自增操作,a的值变为11。紧接着执行c=++a语句,由于后置操作符的特性,所以首先执行a的自增操作,a的值变为12,然后执行c=a这一赋值操作,所以c的值变为12。当执行d=10*a++语句时,由于++操作符的优先级大于*操作符,所以该语句等价于d=10*(a++);由于是后置操作符,所以首先执行赋值语句,d的值变为10*12,即为120,然后a执行自增操作,变为13,所以最终a、b、c、d的值分别变为13、10、12、120。

再如:

首先定义int a=4,然后分别执行以下5种情况:

(1)a+=a++;

(2)a+=++a;

(3)++a+=a;

(4)++a+=a++;

(5)++a+=++a;

在VC 6.0的环境下执行以上代码,第(1)种情况下,a的值变为9;第(2)种情况下,a的值变为10;第(3)种情况下,a的值变为10;第(4)种情况下,a的值变为11;第(5)种情况下,a的值变为12。

需要注意的是,对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符,一般推荐使用前置自增运算,因为前置自增(++i)通常要比后置自增(i++)效率更高。

a是变量,执行(a++)+=a语句是否合法

为了更好地说明本题,首先引入两个概念:左值和右值。左值就是可以出现在表达式左边的值(等号左边),可以被改变,它是存储数据值的那块内存的地址,也称为变量的地址;右值是指存储在某内存地址中的数据,也称为变量的数据。左值可以作为右值,但是右值不可以是左值。

本题不合法,a++不能当做左值使用。++a可以当左值使用。++a表示取a的地址,对它的内容进行加1操作,然后把值放在寄存器中。a++表示取a的地址,把它的值装入寄存器,然后对内存中a的值执行加1操作。

所以,对于如下两种写法:(1)i++=5;(2)++i=5;第(1)种写法是错误的,第(2)种写法是正确的。i++的运算结果并不是i这个变量的引用,而是一个临时变量,其值为i的值,所以无法进行i++=5运算,甚至编译器不允许对一个临时变量重新赋值,上面的表达式会引起编译错误。

 如何进行float、bool、int、指针变量与“零值”的比较

在编写程序时,经常需要对变量与“零值”进行比较判断。考查对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变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

具体而言,分以下几种情况。

(1)int类型。

if(n==0)
if(n!=0)

不推荐的写法有:

if(n)
if(!n)

因为这样写容易让人误解n是布尔变量。

(2)float类型。无论是float还是double类型的变量,由于它们在内存中的存储机制与整型数不同,有舍入误差,所以在计算机中,大多数浮点数都是无法精确表达的,很难用A==B来判定两个浮点数是否相同。在判断浮点数相等时,推荐用范围来确定,若x在某一范围内,就认为相等,至于范围怎么定义,要依据实际情况而定,float和double也各有不同。所以都不可以用“==”或“!=”与任何数字比较,应该设法转化成“>=”或“<=”某个精度值。具体方式如下所示:

const float EPSINON = 0.00001;
if((x>=- EPSINON) && (x <= EPSINON)

上例中的EPSINON的值取的是0.00001,而一般对于该值的选取主要是按照实际情况设置的。

错误的写法有以下两种形式:

if(x==0.0)
if(x!=0.0)

需要注意的是,因为浮点数的精度误差,导致对于确切的两个浮点数a与b,a+b的值和b+a的值永远是相等的,而浮点数的运算是不可结合的,所以(a+b)+c的值和(a+c)+b的值就不一定相等了。

(3)bool类型。

if(flag)
if(!flag)

不推荐的写法有:

if(flag==TRUE)
if(flag==FALSE)
if(flag==1)
if(flag==0)

(4)指针类型。

if(p==NULL)
if(p!=NULL)

不推荐的写法有:

if(p==0)
if(p!=0)

上述写法容易让人误解p是整型变量。

if(p)
if(!p)

上述写法容易让人误解p是bool型变量。

在进行比较时,有一个比较容易忽略的问题,就是将双等号“==”与单等号“=”混淆。其实与日常生活中不同的是,在计算机领域,单等号“=”表示的是赋值操作,而双等号“==”才表示比较操作。

new/delete与malloc/free的区别是什么

在C++中,申请动态内存与释放动态内存,用new/delete与malloc/free都可以,而且它们的存储方式相同,new与malloc动态申请的内存都位于堆中,无法被操作系统自动回收,需要对应的delete与free来释放空间,同时对于一般的数据类型,如int、char型,它们的效果一样。

malloc/free是C/C++语言的标准库函数,在C语言中需要头文件<stdlib.h>的支持,new/delete是C++的运算符。对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建的同时要自动执行构造函数,对象消亡之前要自动执行析构函数,而malloc/free不在编译器控制权限之内,无法执行构造函数和析构函数。

具体而言,new/delete与malloc/free的区别主要表现在以下几个方面:

(1)new能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。例如,int*p1=new int[2],int*p2=malloc(2*sizeof(int))。

(2)new与delete直接带具体类型的指针,malloc与free返回void类型的指针。

(3)new是类型安全的,而malloc不是,例如,int*p=new float[2],编译时就会报错;而int*p=malloc(2*sizeof(float)),编译时编译器就无法指出错误来。

(4)new一般由两步构成,分别是new操作和构造。new操作对应于malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。

(5)new将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。

(6)malloc/free需要库文件stdlib.h支持,new/delete则不需要库文件支持。

程序示例如下:

#include <iostream>
using namespace std;
class A
{
    public:
    A()
    {
        cout<<"A is here!"<<endl;
    }
    ~A( )
    {
    cout<<"A is dead!"<<endl;
    }
    private:
        int i;
};
int main()
{
    A* pA=new A;
    delete pA;
    return 0;
}

程序输出为

A is here!
A is dead!

需要注意的是,有资源的申请,就有资源的释放,否则就会出现资源泄露(也称内存泄露)的问题,所以new/delete,malloc/free必须配对使用。而且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;
j

程序输出为

world

通过上例可知,free或delete调用后,内存其实并没有释放,也没有为空,而是还存储有内容,所以在将资源free或delete调用后,还需要将其置为NULL才行。

此时,便产生了一个问题,既然new/delete的功能完全覆盖了malloc/free,为什么在C++中没有取消掉malloc/free,而是仍然将其保留了?其实,由于C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存,所以仍然保留了malloc/free。

什么时候需要将引用作为返回值

将引用作为函数返回值类型的格式如下所示:

类型标识符&函数名(形参列表及类型说明){//函数体}

将引用作为返回值的优点是在内存中不产生被返回值的副本,从而大大提高了程序的安全性与效率。

具体而言,将引用作为函数返回值类型的格式一般需要注意以下4点内容:

(1)不能返回局部变量的引用。局部变量由于存储在栈区,在函数返回后会被销毁,因此被返回的引用就成为了“无所指”的引用,程序会进入未知状态,引起程序错误甚至崩溃。

(2)不能返回函数内部new分配的内存的引用。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成内存泄露。

(3)可以返回类成员的引用,但最好是常引用类型。当对象的属性与某种业务规则相关联时,其赋值常常与某些其他属性或对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其他对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

(4)流操作符<<和>>。一般这两个操作符连续使用,因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。在另外的一些操作符中,不能返回引用+-*/四则运算符。由于这4个操作符没有副作用,因此它们必须构造一个对象作为返回值,可选的方案包括返回一个对象,返回一个局部变量的引用,返回一个new分配的对象的引用,返回一个静态对象引用。根据前面提到的引用作为返回值的3个规则,第(2),(3)两个方案都被否决了。静态对象的引用又因为((a+b)==(c+d))会永远为true而导致错误,所以可选的只剩下返回一个对象了。

变量名为618Software是否合法

变量名618Software不合法。在C语言中,变量名、函数名、数组名统称为标识符,C语言规定标识符只能由字母(a~z,A~Z)、数字(0~9)、下画线(_)组成,并且标识符的第一个字符必须是字母或下画线,不能以数字开头,不能包含除了“_”以外的任何特殊字符,如%、#等,不能包含空白字符(换行符、空格和制表符)。

以下标识符都是非法的。

(1)char:char是C语言的一个数据类型,是保留字,不能作为标识符,其他的如int、float等类似。

(2)number of book:标识符中不能有空格。

(3)3com:以数字开头。

(4)a*b:*不能作为标识符的字符。

值得注意的是,C语言是区分大小写的,例如Count与count被认为是两个不同的标识符,这一点与其他语言不一样。

C语言中,整型变量x小于0,是否可知x×2也小于0

假定计算机是32位的,用2的补码表示整数,若x<0,则x*2<0不一定成立。例如,当x为整型值的最小时就不成立。

程序示例代码如下:

#include <stdio.h>
int main()
{
    int x = -4292967295;
    if (2*x<0)
        printf("2*x<0\n");
    else
        printf("2*x>0\n");
    return 0;
}

程序输出为

2*x>0

exit(status)是否跟从main()函数返回的status等价

在C语言标准中,它们是等价的,但是如果在退出的时候需要使用main()函数的局部数据,那么从main()函数中使用return()就不行了。

表exit函数与return的功能

函数功能
return返回函数调用,如果返回的是main()函数,则为推出程序
exit在调用处强行退出程序,运行一次程序就结束,exit()是程序结束时返回0给系统,正常退出;exit(1)程序结束时返回1给系统;exit(n)程序结束时返回n给系统

对于exit()函数而言,无论参数是几,其效果都是相同的,不同之处在于程序员可以用不同的数字来区别退出的原因,从而方便判断程序的异常问题。例如,内存分配失败是exit(1),打开文件失败是exit(2)或者用来标示在此处退出。

已知String类定义,如何实现其函数体

String类定义如下:

class String
{
    public:
        String(const char *str = NULL);        //通用构造函数
        String(const String &another);         //复制构造函数
        ~String( );                            //析构函数
        String& operator =(const String &rhs); //赋值 函数
    private:
        char *m_ data;                         //用于保存字符串
};

在这个类中包括了指针类成员变量m_data,所以需要自定义其复制构造函数、赋值运算操作符函数,避免单纯的指针值的复制。

具体而言,String类的函数体实现代码如下:

#include <iostream>
using namespace std;
class String
{
    public:
        String(const char *str = NULL);//通用构造函数
        String(const String &another);//复制构造函数
        ~String();//析构函数
        String& operator =(const String &rhs);//赋值函数
    private:
        char *m_data;//用于保存字符串
}

String::String(const char* str)
{
    if(str == NULL)
    {
        m_data = new char[1];
        m_data[0] = '\0'; 
    }
    else
    {
        m_data = new char[strlen(tr)+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;
}

String& String::operator =(const String &rhs)
{
    if(this == &rhs)
        return *this;
    delete []m_data;
    m_data = new char[strlen(rhs.m_data)+1]; 
    strcpy(m_data,rhs.m_data);
    return *this;
}

int main()
{
    String a("abcdefg");
    print("%s\n",a);
    String b(a);
    print("%s\n",b);
    String c=b;
    printf("%s\n",c);
    return 0; 
}

程序输出为

abcdefg
abcdefg
abcdefg

在C++中如何实现模板函数的外部调用

export是C++新增的关键字,它的作用是实现模板函数的外部调用,类似于extern关键字。为了访问其他代码文件中的变量或对象,对普通类型(包括基本数据类、结构和类)可以利用关键字extern来使用这些变量或对象,但对于模板类型,则可以在头文件中声明模板类和模板函数,在代码文件中使用关键字export来定义具体的模板类对象和模板函数,然后在其他用户代码文件中,包含声明头文件后,就可以使用这些对象和函数了。使用方式如下:

extern int n;
extern struct Point p;
extern class A a;
export template <class T> class Stack<int> s;
export template <class T> void f(T&t){...}

 在C++中,关键字explicit有什么作用

在C++中,如下声明是合法的。

class String
{
    String(const char* p);
    ...
}
String s1 = "hello";

上例中,String s1="hello"会执行隐式转换,等价于String s1=String("hello")。为了避免这种情况的发生,C++引入了关键字explicit,它可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。

在C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数)一般具备两个功能:构造器和默认且隐含的类型转换操作符。所以,当AAA=XXX,恰好XXX的类型正好是AAA单参数构造器的参数类型,这时候编译器就自动调用这个构造器,创建一个AAA的对象。而在某些情况下,却违背了程序员的本意。此时就要在这个构造器前面加上explicit修饰,指定这个构造器只能被明确地调用、使用,不能作为类型转换操作符被隐含地使用。

程序代码如下:

class Test 1
{
    public:
        Testl(int n) { num=n; } //普通构造函数
    private:
        int num;
};
class Test2
{
    public:
        explicit Test2(int n) { num= n; }    //explicit(显式)构造函数
    private:
        int num;
}
int main( )
{
    Test1 t1= 12;    //隐式调用其构造函数,成功
    Test2 t2= 12;    //编译错误,不能隐式调用其构造函数
    Test2 t3(12);    //显示调用成功
    return 0; 
}

Test1的构造函数带一个int型的参数,Test1 t1=12会隐式转换成调用Test1的这个构造函数,而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此Test2 t2=12会出现编译错误。普通构造函数能够被隐式调用,而explicit()构造函数只能被显式调用。

C++中异常的处理方法以及使用了哪些关键字

C++异常处理使用的关键字有:try、catch、throw。C++中的异常处理机制只能处理由throw捕获的异常,没有捕获的将被忽略。使用try{}catch(){}语句来捕获异常,把可能发生异常的代码放在try{}语句块中,后面跟若干个catch(){}负责处理具体的异常类型,这样一组有try块和不少于一个的catch块就构成了一级异常捕获。如果本级没有带适当类型参数的catch块,将不能捕获异常,异常就会向上一级传递,函数调用处如果没有捕获住异常,则直接跳到更高一层的调用者,如果一直没有捕获该异常,C++会使用默认的异常处理函数,该函数可能会让程序最终跳出main()函数并导致程序异常终止。

catch的作用是捕获异常,finally不管代码是否有异常都执行。try中如果有return,仍然需要执行finally语句。此种情况的执行过程如下:

(1)执行return返回语句(return之后的语句内容),计算返回值,暂存在一个临时变量中。

(2)执行finally语句块。

(3)return原来已经计算得到的结果值。

如果在finally区段中又调用了一次return语句,则try区段中的返回值将会被遮掩,使得方法调用者得到的是finally区段中的返回值,这常常又与程序编写的初衷相背。

如何定义和实现一个类的成员函数为回调函数

回调函数就是被调用者回头调用的函数,它是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,此时就可以称它为回调函数。回调函数不是由该函数的实现方直接调用的,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个被调用函数。而该被调用函数在需要的时候,利用传递的地址调用回调函数。

回调函数由程序员自己编写,当需要调用另外一个函数时,这个函数的其中一个参数就是这个回调函数名。系统在必要的时候就会调用程序员写的回调函数,这样就可以在回调函数里完成要做的事。

要定义和实现一个类的成员函数为回调函数需要做3件事:

(1)声明。

(2)定义。

(3)设置触发条件,就是在函数中把回调函数名作为一个参数,以便系统调用。

声明回调函数类型示例如下:

typedef void (*FunPtr)(void); 
//定义回调函数
class A
{
    public:
    //回调函数,必须声明为static
    static void callBackFun(void)
    {
        ...
    }
};
//设置触发条件
void Funtype(FunPtr p)
{
    p( );
}
void main(void)
{
    Funtype(A::callBackFun);
}

回调函数与应用程序接口(API)非常接近,它们都是跨层调用的函数,但区别是API是低层提供给高层的调用,一般这个函数对高层都是已知的。而回调函数正好相反,它是高层提供给底层的调用,对于低层它是未知的,必须由高层进行安装,这个安装函数其实就是一个低层提供的API,安装后低层不知道这个回调的名字,但它通过一个函数指针来保存这个回调函数,在需要调用时,只需引用这个函数指针和相关的参数指针。

  • 65
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值