C++关键字

1、static(静态)变量

  在C语言中,static的意思是静态,他有3个明显的作用:

  (1)在函数体内,静态变量具有“记忆”功能,即一个被声明为静态的变量在这一函数被调用的过程中值维持不变。

  (2)在模块内(但在函数体外),他的作用范围是有限制的,但如果一个变量被声明为静态的,那么该变量可以被模块内所有函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。如果一个函数被声明为静态的,那么其作用与仅在本文件内,它只可以被本文件内的其他函数调用,不能被模块外的其他函数调用,也就是说这个函数被限制在本地范围内使用。

  (3)内部函数应该在当前源文件中说明和定义,对于可在当前源文件以外使用的函数,应该在一个头文件中说明,使用这些函数的源文件要包含这个头文件。

  具体而言,static全局变量和普通的全局变量的区别在于static全局变量只初始化一次,这样做的目的是为了防止在其他文件单元中被引用。static局部变量和普通变量的区别在于它只被初始化一次。

 

  在C++中,在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。静态数据成员有以下特点:

  (1)对于非静态成员,每个类对象都有自己的复制品。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份复制品,由该类型的所有对象又该类型的所有对象共享访问。

  (2)静态数据成员存储在全局数据去,定义时要分配控件,所以不能在类声明中定义。由于静态数据成员属于本类的所有对象共享,所以它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,程序员也可以使用它。

  (3)静态数据成员和普通数据成员一样遵从Public、protected、private访问规则。

  (4)static成员变量的初始化是在类外,此时不能再带上static的关键字。private、protected的static成员虽然可以在类外初始化,但是不能在类外被访问。

   与全局变量相比,使用静态变量有以下两个优势:

  (1)静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其他全局名字冲突的可能性。

  (2)可以实现信息隐藏。静态数据成员可以是private,而全局变量不能。

引申问题:

1、为什么static变量只初始化一次?

  因为静态变量具有记忆功能,初始化后,一直没被销毁,而是保存在内存区域中,所以不会再次初始化。

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

  不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也会引起程序错误。

 

2、const有哪些作用

  常类型也被成为const类型,是指使用类型修饰符const说明的类型。const是C和C++中常见的关键字,在C语言中,它主要用于定义变量为常类型以及修饰函数参数和返回值,而在C++中还可以修饰函数的定义,定义类的成员函数。常类型的变量或对象的值是不能更新的。

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

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

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

   (2)进行类型检查,是编译器对处理内容有更多的链接,消除了一些隐患。例如:

void f(const int 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(int i )const {...}//上一个函数的重载  
}    

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

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

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

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

  

引申问题:

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

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

int const x=2;
或
const  int x=2;

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

int const a[0]={1,2,3,4,5,6,7,8};
const int a[0]={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)

  告诉编译器Var在函数体中的无法改变,从而防止了使用者一些无意的或错误的修改。

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

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

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

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

  这y样,在调用函数Fun()时就不能修改类或对象的属性。

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

extern const int i;
extern const int j=10;

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

 

 2、引申2:什么是常引用?

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

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

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

  常引用的主要用途如下:

  1. 用作普通变量只读属性的别名。通常这个别名智能获得这个变量的值,而不能改变这个变量的值。
  2. 用作函数的形参。这样可以保证在函数内不会改变实参的值,所以参数传递时要尽量使用常引用类型。

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

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

1、double & dr=1;
2、const double &cdr=1;

  第一种方式错误,初始化值不是左值。而第二种方式正确,执行过程如下:

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

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

const int ival = 1024;
1、const int & refVal = ival;
2. int & ref2 = ival;

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

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

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

void bar(string &s);

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

bar("hello world")

  程序实例如下:

#include <iostream>
#include <string>

using namespace std;

void bar(string &s)
{
  cout<<s<<endl;
}

int main()
{
  var("hello world");
  return 0;
}

  程序输出为

    hello world

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

 

3、volatile在程序设计中的作用

   编译器优化的时候可能会出现问题,如当遇到多线程编程时,变量的值可能因为别的线程而改变了,而该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。例如:在本次线程内,当读取一个变量时,为提高存取熟读,编译其优化过程中有时会先把变量量读取到一个寄存器内;当以后再取变量值时,就直接从寄存器中取值;当变量值再本线程里改变时,会同时把变量的新值复制到该寄存器中,以便保持一致。

  volatile是一个类型修饰符,它用来修饰被不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到它时都是直接从对应的内存中提取,而不会利用cache中的原有值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化。所以,volatile一般用于修饰多线程间被多个任务共享的变量和并行设备硬件寄存器等。

  对于volatile关键字的作用,可以通过再代码中插入汇编代码,测试有无volatile关键字对程序最终代码的影响。

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

#include <stdio.h>
int main()
{
  int i=10;
  int a=i;
 print("i=%d\n",a);//下面汇编语句的作用是改变内存中i的值,但是又不让编译器知道
  _asm
{
   mov  dword ptr [ebp-4],20h
}
int b=i;
print('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;
 print("i=%d\n",a);//下面汇编语句的作用是改变内存中i的值,但是又不让编译器知道
  _asm
{
   mov  dword ptr [ebp-4],20h
}
int b=i;
print('i=%d\n",b);
return 0;
}

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

i=10
i=32

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

4、断言ASSERT()是什么

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

  其用法如下:

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)每个asset()一般只检验一个条件,而不对多个条件进行检验,因为同时检验多个条件时,如果断言失败,则无法直观的判断是哪个条件失败。例如,assert(nOffset>=0&&noffset+nSize<=m_nInformationSize)就不是一种高效的方式。将其分开更好

assert(nOffset>=0);
assert(noffset+nSize<=m_nInformationSize);

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

assert(i<100);
i++;

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

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

5、main()中带的参数argc与argv的含义是什么?

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

  命令有时用来启动一个程序的执行,如int main(int argc,char *argv[])

  其中第一个参数表示明亮行参数的数目,它是int型的;

  第二个参数argv是一个指向字符串的指针数组,由于参数的数目并没有内在的限制,所以argv指向这组参数值(从本质上是一个数组)的第一个元素,这些元素中的每个都是指向一个参数文本的指针。

 

6、前置运算和后置运算有什么区别?

  ++a表示取a的地址,增加它的内容,然后把值放到寄存器中。

  a++表示取a的地址,把它的值装到寄存器中,然后增加内存中a的值。

 

7、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 "stdafx.h"
#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; }

  运行结果如下:

   需要注意的是,有资源的申请,就有资源的回收,否则就会出现资源泄露(也称内存泄漏问题)的问题,所以new/delete必须配对使用。而且delete和free被调用后,内存不会立即收回,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是,由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生改变,出现野指针的情况。因此,释放完内存后,应该将指针指向位置为空。

  程序示例如下:

#include "stdafx.h"
#include<stdlib.h>
#include<stdio.h>
#include<string.h>

void TestFree()
{
	char *str = (char*)malloc(100);
	strcpy(str, "helo");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf("%s", str);
	}
}
	int main()
	{
		TestFree();
		return 0;
	}

  程序运行结果如下:

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

  此时,便产生了一个问题,既然new/delete的功能完全覆盖了malloc/free,为什么在C++中没有取消掉malloc/free呢?

  因为C++常要调用C,而C只能用malloc/.free管理动态内存,所以保留了。

 

8、在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){...}

9、在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 Test1
{
public:
	Test1(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型的参数,Test t1=12会隐式转换成调用Test1的这个构造函数,而Test2的构造函数被声明为explicit(显示),这表示不能通过隐式转换调用这个构造函数,因此Test2 t2=12会出现编译错误。普通构造函数能够被隐式调用,而explicit构造函数之只能被显式调用。

 

 

10、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区段中的返回值,这常常与程序编写的初衷相违背。

 

posted @ 2018-07-13 15:18  noticeable  阅读(449)  评论(0编辑  收藏  举报