参考博客地址
宏是C/C++所支持的一种语言特性,我对它最初的印象就是它可以替换代码中的符号,最常见的例子便是定义一个圆周率PI,之后在代码中使用PI来代替具体圆周率的值。
确实如此,宏提供了一种机制,能够使你在编译期替换代码中的符号或者语句。当你的代码中存在大量相似的、重复的代码时,使用宏可以极大的减少代码量,便于书写。
在很多书上以及网文上,宏都是不被推荐使用的,因为它会带来一些隐晦的坑,让你不经意间便受其所困。但是,它出现总有它的道理。
C语言中的NULL就是一个语言已经预定义的宏。预定义指的是你不必亲自定义,编译器在编译时,已经提前定义好了。
// 定义圆周率
#define PI 3.14159265
// 定义一个空指针
#define NULL ((void*)0)
// 定义一个宏的名字为 SYSTEM_API,但是没有值
#define SYSTEM_API
double perimeter = diameter * 3.14159265; // 等价于
double perimeter = diameter * PI;
http://c.biancheng.net/view/250.html
在 C++ 中,当定义一个新的类 B 时,如果发现类 B 拥有某个已写好的类 A 的全部特点,
此外还有类 A 没有的特点,那么就不必从头重写类 B,而是可以把类 A 作为一个“基类”(也称“父类”),
把类 B 写为基类 A 的一个“派生类”(也称“子类”)。
这样,就可以说从类 A “派生”出了类 B,也可以说类 B “继承”了类 A。
虚函数的作用是允许在派生类(子类)中重新定义与基类同名的函数,
并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
在基类中函数需要被virtual关键字说明,在子类中定义该虚函数时,virtual关键字可写可不写
但是为了清晰一般都写。
在派生类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,
并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
(1)sizeof的返回值类型为size_t(unsigned int);
(2)sizeof是运算符,而strlen是函数;
(3)sizeof可以用类型做参数,其参数可以是任意类型的或者是变量、函数,
而strlen只能用char*做参数,且必须是以’\0’结尾;
(4)数组作sizeof的参数时不会退化为指针,而传递给strlen是就退化为指针;
(5)sizeo是编译时的常量,而strlen要到运行时才会计算出来,且是字符串中字符的个数而不是内存大小;
可以看看 https://www.ccppcoding.com/archives/234654,写的不错
1. 数组要么在全局数据区被创建,要么在栈上被创建;指针可以随时指向任意类型的内存块;
2. 修改内容上的差别:
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
3. 用运算符sizeof 可以计算出数组的容量(字节数)。
sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p所指的内存容量。
C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
静态转换、动态转换、常量转换和重新解释转换。
1 静态转换 Static Cast
int i = 10;
float f = static_cast(i); // 静态将int类型转换为float类型
静态转换是将一种数据类型的值强制转换为另一种数据类型的值。
静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。
静态转换不进行任何运行时类型检查,因此可能会导致运行时错误
2动态转换 Dynamic Cast
动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。
动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。
class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast(ptr_base); // 将基类指针转换为派生类指针
3 常量转换 Const Cast
常量转换用于将 const 类型的对象转换为非 const 类型的对象。
常量转换只能用于转换掉 const 属性,不能改变对象的类型。
const int i = 10;
int& r = const_cast(i); // 常量转换,将const int转换为int
4 重新解释转换(Reinterpret Cast)
重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。
重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。
int i = 10;
float f = reinterpret_cast(i); // 重新解释将int类型转换为float类型
const是一个限定符,被const限定的变量其值不会被改变。
所以,const变量必须在定义时就被初始化。
1 可以定义const常量
const int bufSize = 512;
2修饰函数的返回值和形参;
在C++中,还可以修饰函数的定义体,定义类的const成员函数。
被const修饰的东西受到强制保护,可以预防意外的变动,提高了程序的健壮性。
const和#define都可以定义常量,但是const用途更广。
const 常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
const引用是指向const对象的引用。const引用必须被定义为const类型。
const int a = 100;
const int &refa = a; // correct:引用和被引用都是const类型
int &refa = a; // error:引用和被引用const类型
const引用可以被读取但是不可以被修改引用对象,任何对const引用进行赋值都是不合法的,
它适用指向const对象的引用,而非const的引用,不适用于指向const对象的引用。
指针和引用都提供了间接操作对象的功能。
引用必须在创建时被初始化。指针可以在任何时间被初始化。
一旦引用被初始化为一个对象,就不能被指向到另一个对象。
指针可以在任何时候指向到另一个对象。
int a = 100;
int &refa = a; // 正确:&refa引用a
int &refa = b; // 错误:引用对象必须初始化
int &refa = 10; //错误:右值必须是对象
int& b; //错误:不存在空引用,引用必须连接到一块合法内存
试想变量名称是变量附属在内存位置中的标签,您可以把引用当成是变量附属在内存位置中的第二个标签。因此可以通过原始变量名称或引用来访问变量的内容。例如:
例如
int main() {
int a = 5;
int& b = a;
//在这里 & 不是取地址操作符,而是类型标识符的一部分。正如 char* s 也是类型标识符的一部分,表示
//一个指向 char 类型的指针变量,而我们的 int& 表示一个指向 int 类型的引用变量。
cout << &a << endl; // 取a的地址
cout << &b << endl; // 取b的地址
int c = 20;
b = c;
cout << a << endl; // 输出a修改后的值
cout << b << endl; // 输出b修改后的值
return 0;
}
输出
0x7fff5fbff80c
0x7fff5fbff80c
20
20
这里a和b指向相同的值和内存单元。
而由于b是a的引用,一直关联a,不会成为c的引用,而是直接对b赋值,而对b赋值就相当于对a赋值,这是改变a值的另一种方法。
所以指针和引用有赋值行为的差异:指针赋值是将指针重新指向另外一个对象,而引用赋值则是修改对象本身;
后续部分可以参考 可以参考primer书,也可以参考博客 https://blog.csdn.net/qq_40873884/article/details/79632314
指针之间存在类型转换,而引用分const引用和非const应用,非const引用只能和同类型的对象绑定,const引用可以绑定到不同但相关类型的对象或者右值,
创建对象时系统会自动调用构造函数进行初始化工作,
同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,
这个函数就是析构函数。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,
它不会返回任何值,也不能带有任何参数。
析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
下面的实例有助于更好地理解析构函数的概念:
#include
using namespace std;
class VLA{
public:
VLA(int len); //构造函数
~VLA(); //析构函数
public:
void input(); //从控制台输入数组元素
void show(); //显示数组元素
private:
int *at(int i); //获取第i个元素的指针
private:
const int m_len; //数组长度
int *m_arr; //数组指针
int *m_p; //指向数组第i个元素的指针
};
VLA::VLA(int len): m_len(len){ //使用初始化列表来给 m_len 赋值
if(len > 0){ m_arr = new int[len]; /*分配内存*/ }
else{ m_arr = NULL; }
}
VLA::~VLA(){
delete[] m_arr; //释放内存
}
void VLA::input(){
for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
}
void VLA::show(){
for(int i=0; m_p=at(i); i++){
if(i == m_len - 1){ cout<<*at(i)<=m_len){ return NULL; }
else{ return m_arr + i; }
}
int main(){
//创建一个有n个元素的数组(对象)
int n;
cout<<"Input array length: ";
cin>>n;
VLA *parr = new VLA(n);
//输入数组元素
cout<<"Input "< input();
//输出数组元素
cout<<"Elements: ";
parr -> show();
//删除数组(对象)
delete parr;
return 0;
}
运行结果:
Input array length: 5
Input 5 numbers: 99 23 45 10 100
Elements: 99, 23, 45, 10, 100
~VLA()就是 VLA 类的析构函数,它的唯一作用就是在删除对象(第 53 行代码)后释放已经分配的内存。
当类中有指针成员时,一般有两种方式来管理指针成员:
一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;
另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。
智能指针的一种通用实现技术是使用引用计数。
智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。
每次创建类的新对象时,初始化指针并将引用计数置为1;
当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;
对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;
调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
用malloc和free
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间
free(p); //释放内存
用new和delete
int *p = new int;
//分配1个int型的内存空间,new 操作符会根据后面的数据类型来推断所需空间的大小。
delete p; //释放内存
如果希望分配一组连续的数据,可以使用 new[]:
int *p = new int[10]; //分配10个int型的内存空间
delete[] p; // 用 new[] 分配的内存需要用 delete[] 释放,它们是一一对应的。
和 malloc() 一样,new 也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。
为了避免内存泄露,通常 new 和 delete、new[] 和 delete[] 操作符应该成对出现,
并且不要和C语言中 malloc()、free() 一起混用。
在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,
最明显的是可以自动调用构造函数和析构函数,后续我们将会讲解。
malloc/free是C/C++标准库函数,new/delete是C++运算符。他们都可以用于动态申请和释放内存。
对于内置类型数据而言,二者没有多大区别。
malloc申请内存的时候要制定分配内存的字节数,而且不会做初始化;
new申请的时候有默认的初始化,同时可以指定初始化;
对于类类型的对象而言,用malloc/free无法满足要求的。
对象在创建的时候要自动执行构造函数,消亡之前要调用析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制之内,不能把执行构造函数和析构函数的任务强加给它,因此,C++还需要new/delete。
每个 .cpp 都是独立的编译单元。
.cpp 源文件经过预处理,将 #include, #ifndef, #define 之类的预处理指令替换成具体的源码,
得到独立的,没有文件依赖的 C++ 源码。
之后才进入编译过程,生成一个的目标文件。
目标文件在 Unix 平台上后缀为 .o,在 Windows 平台上后缀为 .obj。
编译之后进入链接过程,多个 .o 文件可以链接成库,包括静态库(.a)或者动态库(.so)。
多个库和 .o 文件,经过链接过程,也可以链接成可执行文件。
这整个生成过程,可以粗略分解成,预处理(preprocess),编译(complie),链接(link) 三个过程。
上面从我将这个过程描述成线性的,实际上的编译系统也可以设计成一边预处理一边编译。
但从概念上理解成上一过程的输出是下一过程的输入,这样会容易些。
C++ 标准库的函数很多都是预先编译好的,生成 C++ 程序的时候,只需要链接就行了。
也会有编译选项,选择采用动态链接还是静态链接。
而 STL 之类的模板库通常不能预先编译,采用 #include 源码方式来使用,
经过预处理过程之后,模板库的源码也会被重新编译,链接的时候,模板生成的重复代码剔除。
STL 也会分拆成多个头文件,用到的才会编译。
什么是链接库
计算机中,有些文件专门用于存储可以重复使用的代码块,例如功能实用的函数或者类,
我们通常将它们称为库文件,简称“库”(Library)。
所谓链接库,其实就是将开源的库文件进行编译、打包操作后得到的二进制文件。
虽然链接库是二进制文件,但无法独立运行,必须等待其它程序调用,才会被载入内存
用c语言为例,为大家展示的就是一个函数库:
//myMath.c
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
if (b != 0) {
return a / b;
}
return -1;
}
大部分人不会直接分享源代码。而是分享文件的二进制版本,----链接库
一个完整的 C 语言项目可能包含多个 .c 源文件,项目的运行需要经过“编译”和“链接”两个过程:
编译:由编译器逐个对源文件做词法分析、语法分析、语义分析等操作,最终生成多个目标文件。
每个目标文件都是二进制文件,但由于它们会相互调用对方的函数或变量,还可能会调用某些链接库文件中的函数或变量,编译器无法跨文件找到它们确切的存储地址,所以这些目标文件无法单独执行。
链接:对于各个目标文件中缺失的函数和变量的存储地址(后续简称“缺失的地址”),
由链接器负责修复,并最终将所有的目标文件和链接库组织成一个可执行文件。
注意,一个目标文件中使用的函数或变量,可能定义在其他的目标文件中,也可能定义在某个链接库文件中。
链接器完成完成链接工作的方式有两种,分别是:
无论缺失的地址位于其它目标文件还是链接库,链接库都会逐个找到各目标文件中缺失的地址。采用此链接方式生成的可执行文件,可以独立载入内存运行;
链接器先从所有目标文件中找到部分缺失的地址,然后将所有目标文件组织成一个可执行文件。如此生成的可执行文件,仍缺失部分函数和变量的地址,待文件执行时,需连同所有的链接库文件一起载入内存,再由链接器完成剩余的地址修复工作,才能正常执行。
1 申请方式不同。
栈上由系统自动分配和释放;
堆上有程序员自己申请并指明大小;
2
栈是向低地址扩展的数据结构,大小很有限;
堆是向高地址扩展,是不连续的内存区域,空间相对大且灵活;
3
栈由系统分配和释放速度快;
堆由程序员控制,一般较慢,且容易产生碎片;
https://www.runoob.com/cplusplus/cpp-strings.html
最明显的区别是字符串会在末尾自动添加空字符。
字符串实际上是使用 null 字符 \0 终止的一维字符数组
1进程是程序的一次执行,线程是进程中的执行单元;
2进程间是独立的,这表现在内存空间、上下文环境上,线程运行在进程中;
3一般来讲,进程无法突破进程边界存取其他进程内的存储空间;而同一进程所产生的线程共享内存空间;
4同一进程中的两段代码不能同时执行,除非引入多线程。
1 线程执行开销小,但不利于资源管理和保护;进程则相反,进程可跨越机器迁移。
2 多进程时每个进程都有自己的内存空间,而多线程间共享内存空间;
3 线程产生的速度快,线程间通信快、切换快;
4 线程的资源利用率比较好;
5 线程使用公共变量或者资源时需要同步机制。