文章目录
本章内容概述
本文用于笔者在学习 C++ 中众多关键字和重要的库函数时记录笔记和个人分析,同时对面试中常见问题作出解析。
一、sizeof 和 strlen
strlen 是头文件<cstring>
中的函数,测量字符串的实际长度,不包括尾零,而 sizeof 则测量对象或表达式类型所占内存的大小。
char arr[10] = "Hello C++";
cout << strlen(arr) << endl; //9
cout << sizeof(arr) << endl; //10
在 arr 作为参数传递过程中,strlen 将 arr 作为字符数组类型处理,sizeof 将其作为字符指针处理,因为没有“字符数组”这种数据类型。
strlen 作为库函数,是在程序运行期间进行计算,而 sizeof 是在编译期间进行计算; sizeof 只在乎接收的参数的参数类型,并输出该类型所占空间大小,而 strlen 接收表达式时会对表达式进行计算。
需要注意的是,sizeof(1==1) 表达式,在 C 和 C++ 中结果也并不相同,代码如下:
printf("%d\n",sizeof(1==1); //4
cout<<sizeof(1==1)<<endl; //1
因为在 C 中,不存在布尔类型,因此转化为整型,大小为4;C++ 中布尔类型内存为1。
二、explicit
explicit 关键字声明构造函数,可以使得构造函数不可被隐式调用,从而避免意想不到的情况产生,一般用于声明有参构造。
class B
{
public:
int b;
B(int x)
{ b = x; }
};
B b1 = 10; //发生隐式转换
当构造函数被声明为 explicit 后,该赋值语句会被拒绝。
三、static
1.static 静态全局变量
普通全局变量作用域为整个源程序下的多个文件,可用 extern 扩展,但静态全局变量仅可作用在定义的文件内,只能被初始化一次,不可被 extern 修饰,依靠编译器控制作用域。
2.static 静态局部变量
生命周期为整个文件,与静态全局变量相同,但是作用域仅为定义该变量的函数体内,同样只能初始化一次,外部函数无法访问(栈中)。
3.static 静态函数
静态函数只能在本文件内调用,限制了函数的作用域,使用较少。
4.static 静态成员变量
静态成员变量定义在类内,但是初始化必须在类外,不可在类内(构造函数或者初始化列表)初始化,简单理解,在类内初始化则每次实例化对象时都会访问该静态成员变量,但是静态成员变量属于类,而不属于任何成员变量,因此必须在类外声明作用域后初始化,需要注意,在类外初始化时无需再添加关键字 static 可以通过类或者对象访问静态成员变量。
并且,静态成员变量的数据类型可以是所属类的类型,但是普通成员变量不可以。因为静态成员变量在类被定义时,所处空间就已经确定;但是普通成员变量的存储空间是在实例化对象时才被分配的。如果普通成员变量类型是所属类型,则在实例化对象时,陷入无法确定为对象分配空间的死循环中。
5.static 静态成员函数
静态成员函数只能调用静态成员变量,因为静态成原函数没有 this 指针,无法判断是哪个对象或类本身在调用,因此只能访问静态成员变量。
四、const
const 表示不可修改,可用于修饰变量、函数和函数参数。
1.const 变量
const 修饰变量,表明变量不可被修改,相比于宏,const 会进行类型检查,节省内存空间,提高效率。
相比于 define,const 定义了一个常变量(并非“常量”),带有类型,会在编译期间进行类型检查,但 define 只进行字符替换,在预处理期间完成,不存在类型检查,某种意义上不安全;define 不分配内存,直接进行替换,会多次拷贝,占用代码段空间,但 const 会在内存中分配空间且只分配一次,不再修改,因此 const 还可以节约空间。
2.const 指针
const 修饰指针有两种情况:指针常量和常量指针。
指针常量,代码如下:
//指针型常量,指针指向无法修改
int i = 10;
int* const pi = &i;
常量指针,代码如下:
//常量的指针,指针指向的地址存储的值无法修改
int i = 10;
const int* pi=&i;
3.const 成员变量
常成员变量,类内定义,必须列表初始化
,且不能在类的声明中初始化,因为该变量仅针对该对象不可变,但类可以有很多个对象,彼此之间可以不同。若在(此处实验证明)。
4.const 函数参数和返回值
const 修饰函数参数,表示在函数内部不对该变量进行任何修改,多用于引用传递或地址传递。
const 修饰函数返回值,则要求必须用const 指针或引用接受该返回值,此时指向的内容不可被修改。
5.const 成员函数
const 修饰成员函数,则该函数只能访问常成员变量,以防修改,不能修改成员变量,常用于只读情况。
需要注意的是,常对象只能访问常函数,从而避免修改成员变量,因此,在定义函数时,如果函数内并未修改成员变量,则很有必要声明为 const,但可以修改 mutable 修饰的成员变量。
并且,const 作为函数签名的一部分,可以做为函数重载的依据,如类内重载 [] 运算符时,如果是普通成员函数,则返回引用,可修改;若是常对象调用,则返回值,只可访问。
五、inline
1.基本使用
inline 关键字用于将函数声明为内联函数。内联函数,向普通函数一样被调用,但是调用方式不同。普通函数被调用时,会独立开辟栈空间,在栈内操作完成后,返回至被调用点继续执行,因此需要额外的资源(保存被调用时刻的状态以便返回);而内联函数则是指直接在被调用点展开,即使用当前所在位置的资源,无需开辟新的栈空间,减少函数调用带来的开销,从而提高程序运行效率。
单纯的文本替换,似乎 define 也可以做到,但是 define 内部无法检测参数正确性、语法规则额,而 inline 函数则会像普通函数一样被编译器校验。
内联函数定义在头文件中,可以被多个文件包含,但需要注意的是,inline 必须声明在函数定义体处,仅声明在函数声明处是无效的。
类内成员函数默认为内联函数,类内声明、类外定义的成员函数需要在函数体处声明 inline。但是,虚函数不可被声明为内联函数,因为编译阶段无法确定虚函数调用。
目前,inline 一般用于建议编译器将该函数作为内联函数,但是否真的被作为内联函数,仍需编译器决定。
2.工作原理
内联函数在被调用时,不发生控制转移关系,在编译阶段直接将函数体嵌入到每次调用过程中,而不是像调用普通函数时进行地址转换等操作,因此节省开销。但也会导致同样的代码段会频繁出现,二进制可执行文件变大;并且每次修改内联函数定义都会导致每处调用重新嵌入;导致内存抖动。
因此,内联函数一般只适用于较为短小的函数,编译器可拒绝不合适的内联函数请求,如函数中存在循环、低硅、静态变量、存在返回类型、包含选择语句等情况,会被编译器拒绝内联函数请求。
六、New and Delete
1.New 函数
在前文中我们谈到,利用 new 函数在堆区开辟空间,分析一下 new 函数的执行逻辑,代码如下:
complex* pc = new complex(1, 2);
这段代码是在向堆区申请空间,以存放 pc 指向的 complex 类对象,在编译器则会将其转化为如下方式:
//申请合适大小的内存
void* pc = operator new(sizeof(complex)); //内部调用 malloc
//强制类型转换
pc = static_cast<complex>(mem);
//调用构造函数
pc->complex::complex(1, 2);
2.Delete 函数
delete 函数专门用于释放 new 函数在堆区开辟的空间,如果在堆区开辟的空间没有被合法释放,则会导致内存泄露。同样,分析 delete 函数执行逻辑,代码如下:
AString* ps = new AString("hello");
delete ps;
上述代码在堆区开辟了空间存放 ps 指向的字符串对象,然后释放该空间,在编译器内部将被转化为:
AString::~AString(ps);
operator delete(ps);
即,首先调用字符串对象的析构函数,在析构函数内部将会释放类内的堆区空间,然后在调用系统内部的 operator delete 函数,释放 ps 指向的内存。
需要注意的是,array new 函数必须搭配 array delete 函数使用,否则会造成内存泄露,详细内容不在此展开,后续课程会详细分析。
3.new & malloc
malloc 作为C语言库函数,用于从对中申请指定大小的且连续的内存空间,但仅作为申请指定大小空间的工具,不负责对空间的使用负责,即不会调用对象的构造函数进行初始化,但 new 在前文提到,在获得指定大小空间并将其转化为指定对象的指针后,会利用指针调用构造函数进行初始化。
new 可以指定空间内存初始化对象,而 malloc 只能从堆中获取指定大小的内存空间;new 是 C++ 中的一个操作符,malloc 是C的库函数;new 返回一个对象类型的指针,malloc 返回空指针;new 分配失败抛出异常,malloc 返回失败返回空指针;new 会自动计算对象所需空间大小, malloc 需要指定大小;new 作为运算符可以重载, malloc 不支持重载;malloc 申请空间后可以更改大小,new 申请完成后不可以。
4.delete & free
free 用于释放堆中申请的空间,但只能释放 malloc、calloc、realloc 申请的空间,但是仅释放空间,不会将参数指针置空,需要手动处理。
delete 作为关键字,可以被重载,且会在释放空间前调用空间中存储对象的析构函数,在将空间释放,释放后仍需将指针置空。
七、struct
1.C struct & C++ struct
在 C 中,struct 仅作为用户自定义数据类型的一种集合,仅能包含数据类型,形成一个整体,且不能控制成员访问权限;在 C++ 中,struct 与 class 相同,支持成员函数和访问权限,支持继承与多态,使用时也有区别,代码如下:
struct A {...};
struct A a1; //在 C 中定义
A a1; //在 C++ 中定义
2.class & struct
在 C++ 中,struct 和 class 基本相同,都支持继承、多态、访问权限控制、成员函数等,但是部分不同,class 中默认权限为私有,struct 中默认权限公开,继承关系权限也是如此。并且,class 可以定义模板参数,struct 不可以。
八、memcpy & memmove
memmove() 可以拷贝字节,但是在重叠内存块这方面,memmove() 是比 memcpy() 更安全的方法。当目标区域和源区域存在重叠,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和 memcpy() 函数功能相同,因此,memmove 是更为安全的函数。
具体处理方式是,如果出现重叠区域,则从后向前拷贝,从而避免出错。
本章总结
本章介绍了许多 C++ 面试中重要的关键字并进行了分析,重点是 static、const、new 等,需要多加复习,熟记于心。
最后,我是Alkaid#3529,一个追求不断进步的学生,期待你的关注!