strlen 是头文件中的函数,sizeof 是 C++ 中的运算符。
strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。
strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;
sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。
lambda 表达式的定义形式如下:
[capture list] (parameter list) -> reurn type
{
function body
}
其中: capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空,但如果函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。 return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
举例:lambda 表达式常搭配排序算法使用。
#include
#include
#include
using namespace std;
int main()
{
vector arr = {3, 4, 76, 12, 54, 90, 34};
sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序
for (auto a : arr)
{
cout << a << " ";
}
return 0;
}
/*
运行结果:90 76 54 34 12 4 3
*/
作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。
隐式转换:
#include
#include
using namespace std;
class A
{
public:
int var;
A(int tmp)
{
var = tmp;
}
};
int main()
{
A ex = 10; // 发生了隐式转换
return 0;
}
上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex,等同于如下操作:为了避免隐式转换,可用 explicit 关键字进行声明:
#include
#include
using namespace std;
class A
{
public:
int var;
explicit A(int tmp)
{
var = tmp;
cout << var << endl;
}
};
int main()
{
A ex(100);
A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
return 0;
}
作用:static 定义静态变量,静态函数。
保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。
#include
using namespace std;
int fun(){
static int var = 1; // var 只在第一次进入这个函数的时初始化
var += 1;
return var;
}
int main()
{
for(int i = 0; i < 10; ++i)
cout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11
return 0;
}
隐藏:static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)
static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。
#include
using namespace std;
class A
{
private:
int var;
static int s_var; // 静态成员变量
public:
void show()
{
cout << s_var++ << endl;
}
static void s_show()
{
cout << s_var << endl;
// cout << var << endl; // error: invalid use of member 'A::a' in static member function. 静态成员函数不能调用非静态成员变量。无法使用 this.var
// show(); // error: cannot call member function 'void A::show()' without object. 静态成员函数不能调用非静态成员函数。无法使用 this.show()
}
};
int A::s_var = 1; // 静态成员变量在类外进行初始化赋值,默认初始化为 0
int main()
{
// cout << A::sa << endl; // error: 'int A::sa' is private within this context
A ex;
ex.show();
A::s_show();
}
static 静态成员变量:
静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。
静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
#include
using namespace std;
class A
{
public:
static int s_var;
int var;
void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
void fun2(int i = var); // error: invalid use of non-static data member 'A::var'
};
int main()
{
return 0;
}
静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。
#include
using namespace std;
class A
{
public:
static A s_var; // 正确,静态数据成员
A var; // error: field 'var' has incomplete type 'A'
A *p; // 正确,指针
A &var1; // 正确,引用
};
int main()
{
return 0;
}
static 静态成员函数:
静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。
相同点:
存储方式:普通全局变量和 static 全局变量都是静态存储方式。
不同点:
作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的;静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
初始化:静态全局变量只初始化一次,防止在其他文件中使用。
作用:
const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
const 修饰函数参数,使得传递过来的函数参数的值不能改变。
const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
在类中的用法:
const 成员变量:
const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。
const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。
const 成员函数: 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
不能调用非常量成员函数,以防修改成员变量的值。
#include
using namespace std;
class A
{
public:
int var;
A(int tmp) : var(tmp) {}
void c_fun(int tmp) const // const 成员函数
{
var = tmp; // error: assignment of member 'A::var' in read-only object. 在 const 成员函数中,不能修改任何类成员变量。
fun(tmp); // error: passing 'const A' as 'this' argument discards qualifiers. const 成员函数不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
}
void fun(int tmp)
{
var = tmp;
}
};
int main()
{
return 0;
}
原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。
typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
功能:typedef 用来定义类型的别名,方便使用。
#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
指针的操作:typedef 和 #define 在处理指针时不完全一样。
#include
#define INTPTR1 int *
typedef int * INTPTR2;
using namespace std;
int main()
{
INTPTR1 p1, p2; // p1: int *; p2: int
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。
return 0;
}
#include
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;
int main ()
{
int var1 = 10, var2 = 100;
cout << MAX(var1, var2) << endl;
cout << MIN(var1, var2) << endl;
return 0;
}
/*
程序运行结果:
100
10
*/
作用:
inline 是一个关键字,可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。
使用方法:
类内定义成员函数默认是内联函数
在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:
#include
using namespace std;
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun(){
cout << var << endl;
}
};
int main()
{
return 0;
}
类外定义成员函数,若想定义为内联函数,需用关键字声明
当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。
#include
using namespace std;
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun();
};
inline void A::fun(){
cout << var << endl;
}
int main()
{
return 0;
}
另外,可以在声明函数和定义函数的同时加上 inline;//也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。
关于inline的补充
内联函数的作用:
消除函数调用的开销。 在内联函数出现之前,程序员通常用 #define 定义一些“函数”来消除调用这些函数的开销。内联函数设计的目的之一,就是取代#define 的这项功能(因为使用 #define 定义的那些“函数”,编译器不会检查其参数的正确性等,而使用 inline 定义的函数,和普通函数一样,可以被编译器检查,这样有利于尽早发现错误)。
去除函数只能定义一次的限制。内联函数可以在头文件中被定义,并被多个 .cpp 文件 include,而不会有重定义错误。这也是设计内联函数的主要目的之一。
关于减少函数调用的开销:内联函数一定会被编译器在调用点展开吗?
错,inline 只是对编译器的建议,而非命令。编译器可以选择忽视 inline。当程序员定义的 inline 函数包含复杂递归,或者 inlinie 函数本身比较长,编译器一般不会将其展开,而仍然会选择函数调用。
“调用”普通函数时,一定是调用吗?
错,即使是普通函数,编译器也可以选择进行优化,将普通函数在“调用”点展开。
既然内联函数在编译阶段已经在调用点被展开,那么程序运行时,对应的内存中不包含内联函数的定义,对吗?
错。 首先,如第一点所言,编译器可以选择调用内联函数,而非展开内联函数。因此,内存中仍然需要一份内联函数的定义,以供调用。
而且,一致性是所有语言都应该遵守的准则。普通函数可以有指向它的函数指针,那么,内联函数也可以有指向它的函数指针,因此,内存中需要一份内联函数的定义,使得这样的函数指针可以存在。
内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。
普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。
内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
#include
#define MAX(a, b) ((a) > (b) ? (a) : (b))
using namespace std;
inline int fun_max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int var = 1;
cout << MAX(var, 5) << endl;
cout << fun_max(var, 0) << endl;
return 0;
}
/*
程序运行结果:
5
1
*/
new 是 C++ 中的关键字,用来动态分配内存空间,实现方式如下:
int *p = new int[5];
malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bad_alloc 异常。
delete 的实现原理:
首先执行该对象所属类的析构函数;
进而通过调用 operator delete 的标准库函数来释放所占的内存空间。
delete 和 delete [] 的区别:
delete 用来释放单个对象所占的空间,只会调用一次析构函数;
delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。
在使用的时候 new、delete 搭配使用,malloc、free 搭配使用。
malloc、free 是库函数,而new、delete 是关键字。
-new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。
new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。
new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针。
对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。
new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)
堆是c语言和操作系统的术语,是操作系统维护的一块内存。自由存储是C++中通过new和delete动态分配和释放对象的抽象概念。
malloc 的原理:
当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
malloc 的底层实现:
brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
mmap 内存映射原理:
进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。
C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。
C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;
C++ 是在 C
说明:union 是联合体,struct 是结构体。
区别:
联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。
联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。
语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。
#include
using namespace std;
typedef union
{
char c[10];
char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;
typedef union
{
char c[10];
int i; // int 4 字节,按该类型的倍数分配大小
} u22;
typedef union
{
char c[10];
double d; // double 8 字节,按该类型的倍数分配大小
} u33;
typedef struct s1
{
char c; // 1 字节
double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
} s11;
typedef struct s2
{
char c; // 1 字节
char cc; // 1(char)+ 1(char)= 2 字节
double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;
typedef struct s3
{
char c; // 1 字节
double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
char cc; // 16 + 1(char)+ 7(内存对齐)= 24 字节
} s33;
int main()
{
cout << sizeof(u11) << endl; // 10
cout << sizeof(u22) << endl; // 12
cout << sizeof(u33) << endl; // 16
cout << sizeof(s11) << endl; // 16
cout << sizeof(s22) << endl; // 16
cout << sizeof(s33) << endl; // 24
cout << sizeof(int) << endl; // 4
cout << sizeof(double) << endl; // 8
return 0;
}
struct 和 class 都可以自定义数据类型,也支持继承操作。
struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。
当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身, class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。
struct A{};
class B : A{}; // private 继承
struct C : B{}; // public 继承
volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,告知编译器不应对这样的对象进行优化。
volatile不具有原子性。
volatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。
使用 volatile 关键字的场景:
当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;
中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。
volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。
在C++多线程中,volatile不具有原子性;无法对代码重新排序实施限制。
能干什么:告诉编译器不要在此内存上做任何优化。如果对内存有只写未读的等非常规操作,如
x=10;
x=20;
编译器会优化为:
x=20;
volatile 就是阻止编译器进行此类优化。
4.25 extern C 的作用?
当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern "C" 指出任意非 C++ 函数所用的语言。
// 可能出现在 C++ 头文件
extern "C"{
int strcmp(const char*, const char*);
}
C++ 和 C语言编译函数签名方式不一样, extern关键字可以让两者保持统一,这样才能找到对应的函数。
1)extern外部变量:它属于变量声明,extern int a和int a的区别就是,前者告诉编译器,有一个int类型的变量a定义在其他地方,如果有调用请去其他文件中查找定义。
2)static静态变量:简单说就是在函数等调用结束后,该变量也不会被释放,保存的值还保留。即它的生存期是永久的,直到程序运行结束,系统才会释放,但也无需手动释放。
memcpy函数是C和C++标准库中的一个内存操作函数,用于将一个内存区域的内容复制到另一个内存区域。其底层原理主要包括以下几个方面:
内存地址处理
`memcpy`函数通过指针操作内存地址,使用`void*`类型的指针来接收源地址和目标地址,这样可以处理任意类型的数据。
未对齐处理
-对于未对齐的数据,`memcpy`函数可能需要先处理头部和尾部未对齐的字节,确保中间的复制过程是对齐的,以利用硬件指令的优势。
内存重叠处理
当源地址和目标地址存在重叠时,`memcpy`函数的行为是未定义的。这是因为`memcpy`假设源和目标之间没有依赖关系,因此没有做重叠处理。在这种情况下,应该使用`memmove`函数,它能够正确处理内存重叠的情况。
流水线和并行执行
利用CPU的流水线能力和多指令并行执行复制操作,提高整体效率。
综上所述,`memcpy`函数通过多种优化手段,实现了高效的内存复制操作。在实际使用中,应该根据具体的需求和平台特性选择合适的复制策略,以达到最佳的性能。
strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。
strcpy和memcpy都是在C语言和C++语言中用于复制内存块的函数,但它们在使用和效率上有所不同。
strcpy用于将一个以null结尾的字符串从源地址复制到目标地址。它会复制整个字符串,包括null终止符,直到遇到null为止。如果源字符串长度超过目标地址所分配的内存空间,则会导致内存越界和缓冲区溢出问题。
memcpy用于将一段内存块从源地址复制到目标地址,可以复制任意长度的内存块,而不仅限于字符串。memcpy不会关心内存块中是否有null终止符,而只是按照给定的长度复制内存块。因此,使用memcpy时需要确保目标地址有足够的内存空间,否则也会导致缓冲区溢出问题。
在效率方面,memcpy通常比strcpy更快,因为它不需要扫描整个字符串来查找null终止符。另外,memcpy也可以进行一些优化,例如使用字长操作来提高复制速度。但是,由于strcpy具有更简单的语法和更高的可读性,因此在处理字符串时,通常首选strcpy函数。
auto 类型推导的原理:编译器根据初始值来推算变量的类型,要求用 auto 定义变量时必须有初始值。编译器推断出来的 auto 类型有时和初始值类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则。
auto变量的规则是"做函数模板需要做的事情"
C语言:sizeof(1 == 1) === sizeof(1)按照整数处理,所以是4字节,这里也有可能是8字节(看操作系统)
C++:因为有bool 类型
sizeof(1 == 1) == sizeof(true) 按照bool类型处理,所以是1个字节
C语言没有布尔类型,因此按整数
C++有布尔类型,占1字节