今天我们就来介绍一下一些常用的、比较有意思的关键字,或者是有特殊用途的关键字,以供大家参考。
本文的内容在日常编程中都会用到,在面试时可能也会涉及到,建议大家来详细学习一下,特别是一些刚毕业的朋友们!
sizeof用来计算变量等对象占用的内存大小,以字节为单位。这个关键字大家肯定都用过,但有一点需要注意的是,sizeof是C++中的关键字,不是C运行时库中的函数(这一点估计很多人可能都不太清楚,即使工作多年的人可能也不太晓得),并且sizeof返回的值在编译期间就确定了
在这里给大家说个小插曲。之前在一个技术群中,听一个在校大学生说,他在上他们专业开设的《C语言程序设计》课程时,他们老师居然说sizeof是函数,作为老师这都搞不清楚,这位同学顿时就不愿意了,不想跟着这位老师学下去了。
这点确实不够严谨,做学问还是要严谨一点比较好。其实这也是很多高校IT专业普遍存在的问题,很多任课老师缺乏软件开发的项目经验,对很多知识点的理解不够深入,很难做到理论与实际项目相结合。
malloc和free、new和delete是用来对动态堆内存进行管理的,malloc和free是C语言中的关键字(被C++继承了),new和delete则是C++中的关键字。使用malloc和new去动态申请堆内存,使用free和delete去释放堆内存。
C++程序的大部分数据都是存储在堆上的,即大部分内存使用都是堆内存,必然要使用到new和delete,对堆内存的操作都是使用指针去完成的,指针是C++中的核心类型之一。使用堆内存所引发的一系列的内存问题也是C++中一个重点难点问题。堆内存上的异常问题比栈内存上的异常要难查很多。
关于C/C++动态内存管理的专题,可以参考我之前写的文章:
深入详解C/C++动态内存管理
https://blog.csdn.net/chenlycly/article/details/125879661
文章中系统全面地讲述了动态申请堆内存的相关内容,并融入了多年来的软件项目开发实战的经验总结,感兴趣的朋友,可以去详细看一下。
这个关键字在日常编码中会经常使用,使用的场景也比较多。用const修饰对象时,表示对象是固定的常量,是不可变的。具体的控制行为和修饰对象有直接的关系,当修饰变量时,必须在定义时对变量进程初始化,下面我们就来逐一看一下。
const关键字可以用在类型说明符前,也可以用在类型说明符后,用来标记变量的值是不可改变的,比如:
const int a=10;
int const a=10;
当const被用来修饰指针变量时,会涉及到两个概念,一个是常量指针,一个是指针常量,到底属于哪种,取决于const放在*号的前面还是后面。
如果将const放置在*的前面,如下:
int a = 10;
const int* p = &a;
那这个指针就叫做常量指针,常量指针是指其指向的内容是常量,只是代表不能通过指针变量去修改指向的内存中的值,如果对上面示例代码的指针p指向的内存中的内容进行修改,比如:
*p = 11;
编译时会报错:
error C3892: “p”: 不能给常量赋值。
此时可以通过其他的途径去修改指针指向的内存中的值,比如直接操作原始变量:
a = 11;
这个时允许的。
如果将const放置在*的后面,如下:
int a = 10;
int* const p = &a;
那这个指针就叫做指针常量,指针变量本身是个常量,指针变量的值是不可改变的,比如再定义一个int型变量b,将b的地址复制给p:
int b = 10;
p = &b;
这时去编译就会报错的,因为指针p是指针常量,指针变量的值时不可改变的!但可以使用*p修改指针指向的内存中的内容。
1)修饰函数的形参
以函数参数为结构体(或者是类)为例,有设备信息结构体TDeviceInfo,如果直接将结构体对象作为函数的形参,如下所示:
void Func( TDeviceInfo tDevInfo ) // TDeviceInfo是结构体
这样在调用该函数时,会构造一个临时对象tDevInfo,然后将传进来的结构体参数以值传递的方式设置到临时对象中,然后函数中使用该临时对象访问传进来的值,在函数退出后再将临时结构体对下个析构掉。
如果结构体比较大,这个临时对象的构造和析构可能会消耗掉一定的时间和cpu时间。所以,我们为了提高代码的执行效率,我们可以直接将参数定义为引用或者指针,这样就不用构造临时对象了。但设置成结构体指针或引用,Func函数内部就可以随意修改传入的指针或引用指向的内存中的值,这是我们不希望看到的,这个时候就可以用上const了:
void Func( const TDeviceInfo* pDevInfo )
或者
void Func( const TDeviceInfo& tDevInfo )
这样做是为了防止修改指针或引用指向的内存中的内容,即函数Func内部就不能通过传进来的指针和引用,修改对应内存的值了,同时也解决了需要构造临时对象的问题。
对于函数中形参,const用来修饰指针或引用才是有价值的。如果使用如下的代码:
C++ 常用关键字
application/msword
0星
超过10%的资源
26KB
下载
void Func( const TDeviceInfo tDevInfo )
只表示临时构造的tDevInfo对象变量值时不可修改的,这样做没多大意义。
2)修饰函数的返回值
当用const修饰函数返回值时,那么函数返回值的内容是不可修改的。如果返回值的类型不是指针或引用,可以随意赋值的,不管接受变量是否有const编译都不会报错的。
但如果返回值是指针或者引用,则只能赋值给const类型的接收变量,否则编译会报错,比如:
// GetString函数的返回值类型是const char*
const char* GetString();
// 调用GetString函数
char* lpszBuf = GetString()
会报这样的错误:
error C2440: “初始化”: 无法从“const char *”转换为“char *”,
正确的做法是:
const char* lpszBuf = GetString();
如果调用函数返回值为指针或者引用,那么其返回的被调用函数所在类的内部的变量内存地址,返回出去只是给外部读取的,外部不能修改的,所以加上const就可以实现这一目的。
3)修饰C++类的成员函数
使用const来修饰C++类的成员函数时,该成员函数不能修改类中成员变量的值,比如如下的C++类中定义了一个GetLenth成员函数,函数结尾加上了const关键字:
class ClassA
{
public:
ClassA();
~ClassA();
public:
int GetLength() const;
private:
int m_nLength;
};
那么,GetLength函数中不能成员变量m_nLength,否则编译器会报错。
在成员函数中添加const来限制该成员函数不要去修改类中成员变量的值,而后面讲到的mutable关键字就是为了突破这个限制,如果成员变量前面添加了mutable修饰,则const成员函数中时可以修改该变量值的。关于mutable关键字后面我们再细说。
在C++函数中检测到异常时,可以使用throw抛出异常值,抛出的可以是一个int等基本数据类型变量,也可以是一个C++类对象。在函数外部我们可以使用try...catch去捕获异常,针对不同的异常值做处理,也可以只捕获异常,不做响应的处理。
比如以前我们讲过,通过new去申请动态内存时可能会失败,new内部可能会抛出异常,抛出的是一个bad_alloc类对象,然后我们使用try...catch捕获到这个类对象,把类对象的申请内存失败的原因信息打印出来,如下所示:
#include
using namespace std;
int main(){
char *p;
int i = 0;
try
{
do{
p = new char[10*1024*1024];
i++;
Sleep(5);
}
while(p);
}
catch(const std::exception& e)
{
std::cout << e.what() << "\n"
<< "分配了" << i*10 << "M" << std::endl;
}
return 0;
}
还有我们在调用Windows COM组件去实现某一个功能时,COM组件内部可能会抛出异常,如果不捕获不处理异常,则直接会导致软件崩溃。所以我们在使用COM组件时要小心,可以使用try...catch捕获异常,这里有两个作用,一是程序不会因为一个操作崩溃了,二是我们可以在捕获到异常时去释放资源并弹出一个提示框,提示某操作失败了。示例代码如下:
EmOfficeToXpsErr ExcelToXps( CUIString strSrcFilePath, CUIString strXpsFilePath )
{
COleVariant covFalse( (VARIANT_BOOL)FALSE, VT_BOOL );
COleVariant covTrue( (VARIANT_BOOL)TRUE, VT_BOOL );
COleVariant covOne( (short)1 );
COleVariant covOptional( (long)DISP_E_PARAMNOTFOUND, VT_ERROR );
COleVariant covXPSWriter( PRINTER_NAME );
CExcelApplication excelApp;
CWorkbooks workbooks;
CWorkbook workbook;
CWorksheets workSheets;
if ( excelApp.CreateDispatch( _T("Excel.Application") ) == FALSE )// 启动MS Excel
{
if ( excelApp.CreateDispatch( _T("KET.Application") ) == FALSE )// 启动WPS Excel
{
return OTX_START_OFFICE_FAILED;
}
}
// 此处使用try...catch
try
{
excelApp.put_DisplayAlerts( FALSE );// 关闭office提示框
workbooks = excelApp.get_Workbooks();
// 打开Excel文件
workbook = workbooks.Open( strSrcFilePath, covOptional, covOptional, covOptional, covOptional,
covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional,
covOptional, covOptional, covOptional );
long nSheetCount = 0, nSheetIndex = 0, nSheetPrint = 0;
workSheets = workbook.get_Worksheets();
nSheetCount = workSheets.get_Count(); // 获取当前打开的Excel中Sheet数
// 调整每个Sheet为一页宽多页高
for ( nSheetIndex = 0; nSheetIndex < nSheetCount; nSheetIndex++ )
{
CWorksheet workSheet = workSheets.get_Item( COleVariant( (long)(nSheetIndex + 1) ) );
不处理隐藏的工作表,否则Worksheet打印时会发生崩溃(wordsheet打印时才需要设定)
//if (WorkSheet.get_Visible() != -1)//0:xlSheetHidden; 2:xlSheetVeryHidden ; -1:xlSheetVisible
//{
// WorkSheet.ReleaseDispatch();
// continue;
//}
CPageSetup pageSetup = workSheet.get_PageSetup();// 获取页面设置对象
workSheet.Activate();
// 设置打印网格线
pageSetup.put_PrintGridlines( TRUE );
// 设置打印顺序
// 1:Process down the rows before processing across pages or page fields to the right.
// 2: Process across pages or page fields to the right before moving down the rows.
pageSetup.put_Order( 1 );
// 设置打印纸张方向
// 1: Portrait mode
// 2:Landscape mode
pageSetup.put_Orientation( 2 );
// 设置打印纸张
// 9: A4; 8: A3
pageSetup.put_PaperSize( 8 );
// 获取缩放比为100%时的水平分页符个数
pageSetup.put_Zoom( COleVariant( (short)100 ) );
CHPageBreaks hPageBreaks = workSheet.get_HPageBreaks();
long lHPageNum = 0;
lHPageNum = hPageBreaks.get_Count();
// 设置一页宽多页高
// 必须先将Zoom设为false,PagesWide、PagesTall的设置才会生效
pageSetup.put_Zoom( covFalse ); // covFalse指定为VT_BOOL
pageSetup.put_FitToPagesWide( covOne );
pageSetup.put_FitToPagesTall( COleVariant(lHPageNum+1) );
//nSheetPrint++;
//strFilename.Format(_T("%s_%d%s"),strFilePath, nSheetPrint, _T(".xps"));
//excelWorkSheet._PrintOut(covOptional, covOptional, covOptional, covOptional, covXPSWriter, covOptional, covOptional, COleVariant(strFilename));
hPageBreaks.ReleaseDispatch();
pageSetup.ReleaseDispatch();
workSheet.ReleaseDispatch();
}
// 将文档打印成xps
// PrintToFile设置为true,避免相邻Sheet打印质量不同时提示输入xps文件名
workbook._PrintOut( covOptional, covOptional, covOptional, covOptional, covXPSWriter, covTrue, covOptional, COleVariant( strXpsFilePath ) );
workSheets.ReleaseDispatch();
// 关闭文件,SaveChanges设置为false,避免弹出Excel窗口
workbook.Close( covFalse, covOptional, covOptional );
if ( workbooks.get_Count() == 0 )// 打开文档数为0时才结束进程
{
excelApp.Quit();
}
workbook.ReleaseDispatch();
workbooks.ReleaseDispatch();
excelApp.ReleaseDispatch();
// 判断xps文件是否存在
// 打印过程中点击打印进度提示框的取消按钮,导致无法生成xps文件
if ( PathFileExists( strXpsFilePath ) )
{
return OTX_SUCCESS;
}
else
{
return OTX_COM_EXCEPTION;
}
}
catch(...)
{
// 捕获到异常处理,可以给用户弹出提示框
excelApp.Quit();
workbook.ReleaseDispatch();
workbooks.ReleaseDispatch();
excelApp.ReleaseDispatch();
return OTX_COM_EXCEPTION;
}
try...catch是C++中的异常捕获机制,C语言中也有一套异常捕获的结构__try...__except:
__try
{
// guarded code
}
__except ( expression )
{
// exception handler code
}
我们在调用API函数HtmlHelp打开.chm帮助文档时,如果指定路径中的.chm文件不存在(可能是路径不对,也可能是文件被删除了),HtmlHelp函数内部会抛出异常,软件会发生崩溃。后来我们直接添加__try…__except去捕获异常,对调用HtmlHelp的代码添加保护,保证即使.chm文件不存在,也不能让软件发生崩溃,相关代码如下:
bool OpenChmHelpFile( LPCTSTR lpStrPath )
{
HWND hHelpWnd = NULL;
__try
{
hHelpWnd = HtmlHelp( NULL, lpStrPath, HH_DISPLAY_TOPIC, NULL );
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
hHelpWnd = NULL;
}
if ( NULL == hHelpWnd )
{
WriteLog( _T("[OpenChmHelpFile] HtmlHelp execute failed, path [%s]!"), lpStrPath );
return false;
}
return true;
}
在上述代码中,在__except分支条件中直接设置EXCEPTION_EXECUTE_HANDLER,表示此处我们认领并处理这个异常,这样就不会因为异常未得到处理导致软件崩溃了。我们可以在打开失败时弹出一个提示,提示用户打开失败了。
inline是用来修饰函数的,以inline修饰的函数叫做内联函数,比如:
Delphi 关键字详解
application/msword
0星
超过10%的资源
125KB
下载
inline void * __cdecl memchrInternal(const void *buf, int chr, size_t cnt)
{
#ifdef _X86_
void *pRet = NULL;
_asm {
cld // make sure we get the direction right
mov ecx, cnt // num of bytes to scan
mov edi, buf // pointer byte stream
mov eax, chr // byte to scan for
repne scasb // look for the byte in the byte stream
jnz exit_memchr // Z flag set if byte found
dec edi // scasb always increments edi even when it
// finds the required byte
mov pRet, edi
exit_memchr:
}
return pRet;
#else
while ( cnt && (*(unsigned char *)buf != (unsigned char)chr) ) {
buf = (unsigned char *)buf + 1;
cnt--;
}
return(cnt ? (void *)buf : NULL);
#endif
}
编译时C++编译器会在调用内联函数的地方展开,就像展开宏一样,没有函数压栈的开销,内联函数提升程序运行的效率。
因为所有调用内联函数的地方都会被内联函数的函数体实现代码替换掉,这样就会增大编译出来的二进制文件的大小。inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
explicit关键字用来修饰C++类的构造函数。在C++中,如果一个类有只有一个参数的构造函数,C++允许一种特殊的声明类变量的方式。在这种情况下,可以直接将一个对应于构造函数参数类型的数据直接赋值给类变量,编译器在编译时会自动进行类型转换,将对应于构造函数参数类型的数据转换为类的对象。如果在构造函数前加上explicit修饰词,则会禁止这种自动转换。比如如下的圆信息类:
class Circle
{
public:
Circle(double r) : R(r) {}
private:
double R;
};
// 1、使用类定义一个对象
Circle A = 1.23;
上述代码中的:
Circle A = 1.23;
编译器编译时会转换为:
// 编译器再编译时会转换为:
Circle tmp(1.23); // 先构造
Circle A(tmp); //再拷贝构造
如果在构造函数前加上explicit关键字,如下:
class Circle
{
public:
explicit Circle(double r) : R(r) {}
private:
double R;
};
则上面的代码中的Circle A = 1.23;,在编译时就会报错,则只能这样来使用:
Circle A(1.23);
C++关键字mutable是为了突破const关键字的限制而存在的,当一个变量被mutable修饰,那么它被定义为永远可变。比如:
class Circle
{
public:
Circle(double r) : R(r) {}
private:
mutable double R; // 使用mutable关键字
};
这会和const关键字的性质起到冲突吗?const关键字修饰函数的意思是这个函数不修改任何对象内部状态。而mutable关键字修饰数据成员变量表示这个成员变量不属于对象内部状态。
什么时候使用mutable关键字?我们为了使得类的成员函数不去修改类的数据成员变量,用const修饰成员函数,但是,这会将所有的成员变量的修改一棍子打死全部不可在此函数中修改。我们是为了保护一些特定的数据成员变量,如银行类中的用户存款值,用户信息等。但某些信息就没那么重要了,这些数据字段就用mutable来修饰。
asm关键字被用来在C/C++源代码中切入一段汇编代码的,比如:
inline void * __cdecl memchrInternal(const void *buf, int chr, size_t cnt)
{
#ifdef _X86_
void *pRet = NULL;
_asm {
cld // make sure we get the direction right
mov ecx, cnt // num of bytes to scan
mov edi, buf // pointer byte stream
mov eax, chr // byte to scan for
repne scasb // look for the byte in the byte stream
jnz exit_memchr // Z flag set if byte found
dec edi // scasb always increments edi even when it
// finds the required byte
mov pRet, edi
exit_memchr:
}
return pRet;
#else
while ( cnt && (*(unsigned char *)buf != (unsigned char)chr) ) {
buf = (unsigned char *)buf + 1;
cnt--;
}
return(cnt ? (void *)buf : NULL);
#endif
}
一般我们会在一些对执行效率要求比较高的代码中嵌入汇编代码,提高代码的执行效率,汇编代码的执行效率是最高的。比如我们在处理音视频编解码的算法代码中,时常会嵌入一些汇编代码,以提高代码的运行速度。
有人可能会问,为啥直接在源代码中嵌入汇编代码后执行效率会比较高呢?
经过IDE编译出来的二进制文件中也都是汇编指令,你人为的添加一段汇编代码,都是汇编代码,为啥会有执行速度上的差别呢?因为源代码经过编译器编译生成的汇编代码在实现上可能不是最优的,这要依赖编译器,而我们人为地去嵌入汇编代码,可以直接操纵寄存器和汇编指令,编译器不会再去做处理(不再依赖编译器),保证汇编代码是最优的。
static和exrern是C语言中的关键字,C++语言中在处理C++类时做了一定的延伸。extern用来声明外部全局变量,static可以用来声明变量、全局函数及C++类的静态函数。关于extern和static两关键字的详细说明,可以参见之前写的一篇文章:
一文带你彻底搞懂C/C++编程中static与extern两关键字的使用
https://blog.csdn.net/chenlycly/article/details/125703772
文章通过项目实践详细讲述了这两个关键字的使用。
总结:
extern作用:
1.外部变量(即全局变量)函数外部定义的变量,它不属于哪一个函数,它属于一个源程
序文件。其作用域是整个源程序。外部变量实在函数外部定义的,它的作用域为从变量定义处开始,到本程序文件的末尾。如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字 extern对该变量作“外部变量声明”。表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
2.可能会出现多次被extern声明的情况,声明多次是可以的,但定义只能有一次,否则就是重复定义了。
3.对于全局函数, extern关键词的声明是可有可无的,因为函数本身不加修饰的话就是extern的。
static作用:
1.(隐藏)在C语言编程中,static的一个作用就是信息屏蔽。比方说,你自己定义了一个文件,该文件中有一系列的函数以及变量的声明和定义,你希望该文件中的一些函数和变量只能被该文件中的函数使用,那么,你可以在该函数、变量的前面加上static,代表他们只能被当前文件中的函数使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。所以,在不同文件中定义同名的staitc函数是没问题的,不会冲突的。
2.静态局部变量,有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,这时就应该指定局部变量为“静态局部变量”,用关键字 static 进行声明。在函数多次重入的情况下,该变量一直存在,且是有值的。
一、面向过程设计中的static
1、全局静态变量
• 变量在全局数据区分配内存;
• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;
2、局部静态变量
• 变量在全局数据区分配内存;
• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
3、静态函数
• 静态函数不能被其它文件所用;
• 其它文件中可以定义相同名字的函数,不会发生冲突;
二、面向对象的static关键字(类中的static关键字)
1、类的静态数据成员
• 静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。语句int Myclass::Sum=0;是定义静态数据成员;
• 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
• 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
• 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值>
• 类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
• 静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
2、类的静态成员函数
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
• 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)
调用类的静态成员函数。
extern "C"作用:
通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern “C”,表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)
链接:https://blog.csdn.net/weixin_40593838/article/details/122474117