本质是一个指针,该指针指向这个函数 int (*fun)(int x,int y);
函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。函数指针与数据项相似,函数也有地址,也就是说在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。一个函数名就是一个指针,它指向函数的代码,一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用,函数指针还运行将函数作为变元传递给其他函数。
int add(int x,int y){
return x+y;
}
int sub(int x,int y){
return x-y;
}
//函数指针
int (*fun)(int x,int y);
int main()
{
//第一种写法
fun = add; cout << "(*fun)(1,2) = " << (*fun)(1,2) ; //3
//第二种写法
fun = ⊂ cout << "(*fun)(5,3) = " << (*fun)(5,3) << fun(5,3); //2 2
return 0;
}
返回一个指针的函数,本质是一个函数,该函数的返回值是一个指针 int *fun(int x,int y);
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
data->a = a;
data->b = b;
return data;
}
int main()
{
//调用指针函数
Data * myData = f(4,5);
cout << "f(4,5) = " << myData->a << myData->b; //f(4,5) = 4 5
return 0;
}
int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
delete后置为NULL,新建指针时判断是否为NULL,不是则释放并置为NULL,尽量不使用超出作用范围的指针
首先delete指针只是编译器释放该指针所指向的内存空间(该空间可以给其他变量使用),而不会删除这个指针本身。这可能会导致后续申请指针时,系统新建的指针指向的地址可能会跟delete掉的指针相同,此时如果修改delete掉的指针的内容就会导致对新建的指针内容的修改。
所以为了防止这种情况的发生,需要delete掉后立即置为NULL(避免变成野指针),同时在新建指针的时候需要判断新建的指针是否为NULL,为NULL才是申请成功。
对null的delete可以无数次,因为delete会直接跳过NULL
原文链接:https://blog.csdn.net/weixin_42067304/article/details/108451031
悬空指针,指针最初指向的内存已经被释放了的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
指针free或delete之后没有及时置空 => 释放操作后立即置空。
指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果。
总结
程序员能修改调用函数中的数据对象
通过传递引用而不是整个数据–对象,可以提高程序的运行速度
对于使用引用的值而不做修改的函数:
如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;
如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;
如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;
如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。
因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。
符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。
传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
指针可以有多级,引用只有一级
指针可以为空,引用不能为NULL且在定义时必须初始化
指针在初始化后可以改变指向,而引用在初始化之后不可再改变
sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(具体情况还要具体分析)。
引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
计算机系统对基本数据类型合法地址做出的限制,要求某种类型对象的地址必须是某个值(通常是2、4或8)的倍数。对齐跟数据在内存中的位置有关。
(以上三者取最小的一个)
需要字节对齐的原因在于CPU访问数据的效率问题。例,一个整型变量的地址不是自然对齐,如0x00000002,CPU访问这个整型数据需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据。要是变量在自然对齐位置上,则只需要一次就可以取出数据。
对于32位机来说,4字节能够提高CPU访问速度,但是在32位中使用1字节或两字节对齐,会降低访问速度,所以字节对齐需要考虑处理器类型。在VC中默认是4字节对齐的,GCC也是默认4字节对齐,所以需要综合考虑处理器类型和编译器类型。
在设计不同CPU通信的协议时,或者编写硬件驱动程序时寄存器的结构,都需要字节对齐,因为不同编译器生成的代码不一样,所以本身就自然对齐的也要使其对齐。
编译器按照的字节对齐原则:
数据类型为结构体时 ,编译器可能会在结构体字段的分配中插入间隙,以保证结构体中每个元素都满足它的对齐要求。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐,结构体本身也要对齐(就是说结构体总长度需要是结构体有效对齐值的整数倍),因此,有时需要在结构末尾填充空间,来满足结构体的自身对齐。
struct test01
{
char a; //1字节
short b; //2字节
int c; //4字节
long d; //4字节
};
struct test02
{
char a; //1字节
int c; //4字节
long d; //4字节
short b; //2字节
};
test01 t1 = { 0 };
cout << sizeof(t1) << endl; //12字节 因为char和short之间要自动补齐1字节
test02 t2 = { 0 };
cout << sizeof(t2) << endl; //16字节 因为char自动补齐3字节,short自动补齐2字节
struct test
{
int c; //4字节
long d; //4字节
char a; //1字节
}; //char自动补齐3字节,总共是12字节
#pragma pack(2)
struct test02
{
int a; //4字节
long c; //4字节
char b; //1字节
};
#pragma pack() //按两字节对齐,就是10字节
所以适当地编排结构体成员地顺序,可以在保存相同信息地情况下尽可能节约空间。
大端存储:字数据的高字节存储在低地址中
小端存储:字数据的低字节存储在低地址中
在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输。
int main()
{
int a = 0x1234;
char c = (char)(a);//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
if(c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
int a;
char ch;
};
int main(){
endian value;
value.a = 0x1234;//a和ch共用4字节内存空间
if(value.ch == 0x12)
cout << "big endian" << endl;
else if(value.ch == 0x34)
cout << "little endian" << endl;
}
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。
声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
定义:一般在源文件里,具体就是函数的实现过程 写明函数体。
1、 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。
2、条件编译命令最常见的形式为:
\#ifdef
//程序段1
\#else
//程序段2
\endif
它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。
3、 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。
malloc是C中进行内存分配的函数,它的返回类型是void*即空类型指针,常常有这样的用法:
char* pStr=(char*)malloc(100*sizeof(char)) //语句1
int* pInt=(int*)malloc(100*sizeof(char)) //语句2
这里明显做了显式的类型转换。类型匹配尚且没有问题,但是一旦出现语句2就很可能带来一些问题,而这样的转换C并不会提示错误。
如果C++使用得当,它将远比C更有类型安全性。相比于C语言,C++提供了一些新的机制保障类型安全:
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。
浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题!!
public的变量和函数在类的内部外部都可以访问;protected的变量和函数只能在类的内部和其派生类中访问;private修饰的元素只能在类内访问。
派生类可以继承基类中除了构造/析构、赋值运算符重载函数之外的成员,但是这些成员的访问属性在派生过程中也是可以调整的,三种派生方式的访问权限如下表所示:注意外部访问并不是真正的外部访问,而是在通过派生类的对象对基类成员的访问。
基类成员 | public | private | protected | public | private | protected | public | private | protected |
---|---|---|---|---|---|---|---|---|---|
派生方式 | private | protected | public | ||||||
派生类中 | private | 不可见 | private | protected | 不可见 | protected | public | 不可见 | protected |
外部 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 不可见 | 可见 | 不可见 | 不可见 |
派生类对基类成员的访问形象有如下两种:
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问。
私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承,访问规则如下表:
基类成员 | public成员 | private成员 | protected成员 |
---|---|---|---|
内部访问 | 可访问 | 不可访问 | 可访问 |
外部访问 | 不可访问 | 不可访问 | 不可访问 |
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的,访问规则如下表:
基类成员 | public成员 | private成员 | protected成员 |
---|---|---|---|
内部访问 | 可访问 | 不可访问 | 可访问 |
外部访问 | 不可访问 | 不可访问 | 不可访问 |
程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。
有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:
int fun() throw(int,double,A,B,C){...};
这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常。
bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常;
bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常
out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常
coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。
reinterpret_cast (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。
const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:
static_cast < type-id > (expression)
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
把空指针转换成目标类型的空指针
把任何类型的表达式转换成void类型
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全
dynamic_cast (expression)
该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*
在进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。
主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;
动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。
因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。 但是另一个类里面也要相应的进行声明。
使用友元类时注意:
一般类所占的内存大小是非静态数据成员(在全局区)的总和大小 。
空类的大小讲道理应该是0,但实际上是1。就是涉及到一个实例化的问题,空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,所以编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存中就得到了一个独一无二的地址,所以空类所占内存大小为1。
动态多态,会有个虚函数指针,就是指针的大小(64位指针所占内存是8字节)。