修正时间 08-16
复习时间 10-01
静态局部变量
准确的说未被初始化和初始化为0的在bss零区 初始化为其他值的在data区也称为rw区
)全局静态变量
准确的说未被初始化和初始化为0的在bss零区 初始化为其他值的在data区也称为rw区
)静态函数
类的静态成员变量
static修饰的数据成员不能通过调用构造函数来进行初始化
,因此static修饰的数据成员必须在类外进行初始化且只会初始化一次。类的静态成员函数
记忆 别名 大小 初始化 间接 const 多级 ++
当然有人区别野指针和悬浮指针,但是我觉的不是那么重要
补充
:如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
将可能被继承的父类的析构函数设置为虚函数,可以保证我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄露(顺序 派生类 成员函数 基类
)
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
int *fun(int x,int y);
int (*fun)(int x,int y);
fun = &Function;
fun = Function;
x = (*fun)();
x = fun();
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(int argc, char *argv[])
{
QApplication a(argc, argv);
//第一种写法
fun = add;
qDebug() << "(*fun)(1,2) = " << (*fun)(1,2) ;
//第二种写法
fun = ⊂
qDebug() << "(*fun)(5,3) = " << (*fun)(5,3) << fun(5,3);
return a.exec();
}
也没有返回值(包括void),只能有一个析构函数(可以多个构造),不能重载
。如果类中有指针,并且申请了空间,那么最好在析构函数中销毁
char* strcpy(char* dest,const char* src;)
src逐个拷贝到dest 遇到\0结束,因为没有指定长度,可能会造成拷贝越界,造成缓冲区溢出,安全版本是strncpy
静态多态和动态多态
,静态多态主要就是重载,在编译的时候就已经确定;动态多态使用虚函数机制实现的。举个例子:一个父类类型的指针指向一个子类对象,使用父类的指针去调用子类重写的父类中的虚函数的时候,就会调用子类重写过后的函数,在父类中声明为virtual关键字的函数,在子类中重写不需要加virtual也是虚函数
当子类重写虚函数的时候,会把继承到的虚函数表中的地址替换为重写的函数地址
。使用了虚函数,会增加访问内存开销,降低效率。之前专门写过一篇详细的
补充说明
malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
void* malloc(size_t size)//参数代表字节个数
void free(void* pointer)//参数代表内存地址
void func()
{
//开辟一个空间
int* p1=(int*)malloc(sizeof(int));
if(p1==NULL)
{
exit(1);
}
free(p1);
//开辟多个空间
int*p2=(int*)malloc(sizeof(int)*4);
if(p2==NULL)
{
exit(1);
}
free(p2);
void func()
{
//开辟一个空间
int* p1=new int(1);
delete p1;
//开辟多个空间
int*p2=new int[4];
delete []p2;
更详细的描述
当函数从入口函数main函数开始执行时,编译器会将我们
1操作系统的运行状态
,2main函数的返回地址
、3main的参数(右到左)
、4main函数中的变量
、进行依次压栈;当main函数开始调用func()函数时,编译器此时会将
5main函数的运行状态进行压栈
,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈
从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。
#include
using namespace std;
int f(int n)
{
cout << n << endl;
return n;
}
void func(int param1, int param2)
{
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
}
int main(int argc, char* argv[])
{
func(1, 2);
return 0;
}
//输出结果
//2
//1
//var1=1,var2=2
参考链接
参考链接
例子
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
在C语言中struct是只能定义数据成员,而不能定义成员函数的。而在C++中,struct类似于class,在其中既可以定义数据成员,又可以定义成员函数。
在C++中,struct与class基本是通用的,唯一不同的是如果使用class关键字,类中定义的成员变量或成员函数默认都是private属性的,而采用struct关键字,结构体中定义的成员变量或成员函数默认都是public属性的。
在C++中,没有抛弃C语言中的struct关键字,其意义就在于给C语言程序开发人员有一个归属感,并且能让C++编译器兼容以前用C语言开发出来的项目。
一般来说,在函数内对于存在栈上的局部变量的作用域只在函数内部,在函数返回后,局部变量的内存会自动释放。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错;但是如果返回的是局部变量的地址(指针)的话,就会造成野指针,程序运行会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放,这样指针指向的内容就是不可预料,调用就会出错。
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是4
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
return 0;
}
补充
提到sizeof(地址)的值为8,是在64位的编译环境下的,指针的占用大小为8字节;而在32位环境下,指针占用大小为4字节。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
参考连接
宏在编译之前完成替换(预处理),之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
宏定义不要在最后加分号。
宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
宏不检查类型;typedef会检查数据类型。
宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
2021-09-14 修正
指针常量 (先写指针再常量,常量操作的是变量,所有代表变量指向不可变): int * const p //指针常量
int a,b;
int * const p=&a //指针常量
//那么分为一下两种操作
*p=9;//操作成功
p=&b;//操作错误
const int *p = &a; //常量指针
int a,b;
const int *p=&a //常量指针
//那么分为一下两种操作
*p=9;//操作错误
p=&b;//操作成功
当然糙一点 统称为野指针也行
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被用
//----------------------------
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}
类如果声明了虚函数,这个函数是实现了的,即使是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用动态绑定来达到多态的目的(即父类指针指向子类对象,调用子类方法)。而纯虚函数只是在基类中的一个函数定义,即是一个函数声明而已,具体的实现需要留到子类当中。
虚函数在子类里面也可以不进行重写(只有虚方法和抽象方法才能够被重写);但纯虚函数必须在子类去实现。
虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,子类也可以进行覆写,从而完成自己关于此函数的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类去完成。
带纯虚函数的类叫做抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。
#include
using namespace std;
// 抽象类
class Base {
public:
virtual void func() = 0; // 纯虚函数
};
class child1 : public Base {
public:
void func() { cout << "it's child 1" << endl; } // 覆写父类的纯虚函数
};
class child2 : public Base {
public:
void func() { cout << "it's child 2" << endl; } // 覆写父类的纯虚函数
};
int main() {
// Base b; // 纯虚函数无法实例化
Base* b = nullptr; // 父类指针
child1 c1; // 子类对象
child2 c2; // 子类对象
b = &c1;
b->func(); // 多态
b = &c2;
b->func(); // 多态
return 0;
}
[STL]深拷贝和浅拷贝问题(内存泄露+内存未释放+调用拷贝构造的五种情况)
关于什么是内联函数后面还有
使用宏定义的地方都可以使用 inline 函数。
作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
public的变量和函数在类的内部外部都可以访问。
protected的变量和函数只能在类的内部和其派生类中访问。
private修饰的元素只能在类内访问。
2021-09-13
补充
对于成员的访问,一般来说有类内访问和类外访问。类内访问就是类里面的其它成员直接使用成员名进行访问。类外访问,意思是通过形如对象名.成员名的方式进行访问。此时的.可以理解为中文的“的”。当然,->也有相同的含义。所以 单例饿汉模式中 私有化对象 可以再类外 =new 就可以调用构造函数
因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用
。。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化
,在程序运行结束,变量所处的全局内存会被全部回收。而在C++中,初始化时在执行相关代码时才会进行初始化,
主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的
。void* malloc(unsigned int num_size);
int *p = (int *)malloc(20*sizeof(int));申请20个int类型的空间;
//calloc 省去人为计算的时间 malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
//给动态分配的空间分配额外的空间,用于扩充容量。
void realloc(void *p, size_t new_size);
补充 区别一下野指针和悬浮指针
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了
如何避免
参考链接
参考链接
这个写的也很好
class A
{
public:
int func(int p) {};
};
//其中,函数func的原型在编译器看来应该是:
int func(A* const this, int p) {};
this指针在成员函数开始前构造,在成员函数结束后清除。
this指针并不占用对象的空间
this在成员函数开始执行前构造,在成员执行结束后清除。
this指针会因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。
this指针只有在成员函数中オ有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
简单回答如下
this指针是类的指针,指向对象的首地址。
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。
this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。
构造函数
对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数
拷贝构造函数
对象不存在,但是使用别的已经存在的对象来进行初始化
赋值运算符
对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的
#include
using namespace std;
class A
{
public:
A()
{
cout << "我是构造函数" << endl;
}
A(const A& a)
{
cout << "我是拷贝构造函数" << endl;
}
A& operator = (A& a)
{
cout << "我是赋值操作符" << endl;
return *this;
}
~A() {};
};
int main()
{
A a1; //调用构造函数
A a2 = a1; //调用拷贝构造函数
a2 = a1; //调用赋值操作符
return 0;
}
//输出结果
//我是构造函数
//我是拷贝构造函数
//我是赋值操作符
参考链接
参考链接
当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是已经可以被分配给其他变量了,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了。
C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit。
对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
添加了#pragma pack(n)后规则就变成了下面这样:
1、 偏移量要是n和当前变量大小中较小值的整数倍
2、 整体大小要是n和最大变量大小中较小值的整数倍
3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
struct foo {
int a;
int b;
bool operator==(const foo& rhs) *//* *操作运算符重载*
{
return( a == rhs.a) && (b == rhs.b);
}
};
之前的博客
1.用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
2.操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
3.操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
4.操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
5.执行helloworld程序的第一条指令,发生缺页异常
6.操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
7.helloword程序执行puts函数(系统调用),在显示器上写一字符串
8.操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
9.操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
10.视频硬件将像素转换成显示器可接收和一组控制数据信号
11.显示器解释信号,激发液晶屏
12.OK,我们在屏幕上看到了HelloWorld
https://www.zhihu.com/question/270627626/answer/383917556
面这段话的意思就是说,只有模板实例化时,编译器才会得知T实参是什么。编译器在处理模板实例化时,不仅仅要看到模板的定义式,还需要模版的实现体。比如说存在模板CTest, 其类定义式写在CTest.h,类的实现体写在CTest.cpp中。对于模板来说,编译器在处理CTest.cpp文件时,编译器无法预知T的实参是什么,所以编译器对其实现是不作处理的(即CTest.obj中没有为编译器为实现体生成的对应的二进制代码)。现在有main.cpp真正使用了该模板(比方说,生成模板类的一个对象,并调用其函数),如果定义和实现分离,则编译器可以根据定义式生成模板类的对象(因为此处仅仅需要定义式就知道该对象在内存中需要多少空间并进一步分配了),但是调用对象的函数(即真正使用)需要该函数的定义,由于main.cpp仅仅include了模板的声明(所以只能找到该函数的声明),所以无法找到该函数的定义,此时编译器会寄希望于链接器在其他obj文件(这里就是指CTest.obj文件)中寻找该模板的实现体,但是就像之前说过的,CTest.obj中也没有实现体生成的二进制代码。如果定义和实现是在同一个文件(比如说CTest.h)中,那么编译器在编译时就可以寻找到模板的实现体。这里看下面的三个例子。
1) 算术
x = x + y;
y = x - y;
x = x - y;
2) 异或
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
x ^= y ^= x;
void *memcpy(void *dest, const void *src, int n);从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针
char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。
C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
程序员能修改调用函数中的数据对象
通过传递引用而不是整个数据–对象,可以提高程序的运行速度
如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;
如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;
如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;
如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);
如果数据是内置数据类型,则使用指针
如果数据对象是数组,则只能使用指针
如果数据对象是结构,则使用引用或者指针
如果数据是类对象,则使用引用
编译阶段
安全性
内存占用
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const
,或二者同时指定为const;,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值
;,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数
;有时候必须指定其返回值为const类型,以使得其返回值不为“左值”
。类型转换符const_cast
将const类型转换为非const类型;const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。
因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;
use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;
is-A,继承关系,关系具有传递性;
new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n
,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
针对简单类型,delete和delete[]等同
。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
所以我们常常简单的说 使用new 对应delete new【】 对应使用delete【】
拷贝构造函数是函数,赋值运算符是运算符重载。
拷贝构造函数会生成新的类对象,赋值运算符不能。
拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:
Student s;
Student s1 = s; // 调用拷贝构造函数
Student s2;
s2 = s; // 赋值运算符操作
补充:平时常用:类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符
1) 指针参数传递本质上是值传递,它所传递的是一个地址值。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本
(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变
)。
2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。
因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。
而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。
指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。
符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
define和别名typedef的区别
define与inline的区别
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 这两个才是一样的.
静态成员变量从类被加载开始到类被卸载,一直存在;
普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
静态成员变量是全类共享;普通成员变量是每个对象单独享用的;
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
普通成员变量在类中初始化;静态成员变量在类外初始化;
可以使用静态成员变量作为默认实参,
参考链接
\#ifdef 标识符
程序段1
\#else
程序段2
\#endif
在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。
待补充名字查找
确定候选函数
寻找最佳匹配
添参考链接
内联函数应该被视作简易工具函数,对于一些重复利用率高,代码数量较少的功能(例如比大小,交换位置),可以采用内联提高效率。
之前专门写过一篇
预处理就是进行宏替换、文件引入、取出空行和注释等。生成test.i文件
编译:词法分析,语法分析,最后进行代码优化生产汇编语言 。test.s文件
汇编:编译阶段的文件转换成目标文件,也就是机器代码(0,1)test.o文件
链接:将多个目标文件和库文件链接生成可执行文件的过程 a.out
一步执行指令:gcc test.c -o test
补充一个完整版本
(1)预编译 主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:
删除所有的#define,展开所有的宏定义。
处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
删除所有的注释,“//”和“/**/”。
保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。
添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是 能够显示行号。
(2)编译 把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应 的汇编代码文件。
词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分 割成一系列的记号。
语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的 语法树是一种以表达式为节点的树。
语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进
行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定 的语义。 优化:源代码级别的一个优化过程。
目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言 表示。
目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。 (3)汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没
有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过
来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows
下)、xxx.obj(Linux下)。(4)链接 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链 接:
静态链接 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库
中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个
目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西, 在执行的时候运行速度快。
动态链接 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形
成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副 本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运
行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损 失。
静态编译lib
动态链接dll
提供了一个函数的描述信息给可执行文件
(并没有内存拷贝),当程序被夹在到内存里开始运行的时候,系统会在底层创建DLL和应用程序之间的连接关系,当执行期间需要调用DLL函数时,系统才会真正根据链接的定位信息去执行DLL中的函数代码
动态链接的优缺点是:优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。
参考链接,这个更完整
#include
using namespace std;
class A
{
public:
friend void set_show(int x, A &a); //该函数是友元函数的声明
private:
int data;
};
void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员
{
a.data = x;
cout << a.data << endl;
}
int main(void)
{
class A a;
set_show(1, a);
return 0;
}
#include
using namespace std;
class A
{
public:
friend class C; //这是友元类的声明
private:
int data;
};
class C //友元类定义,为了访问类A中的成员
{
public:
void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};
int main(void)
{
class A a;
class C c;
c.set_show(1, a);
return 0;
}
补充几个注意点
友元关系不能被继承
。
友元关系是单向的
,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
友元关系不具有传递性
。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
类中通过使用关键字friend 来修饰友元函数,但该函数并不是类的成员函数
,其声明可以放在类的私有部分,也可放在共有部分。友元函数的定义在类体外实现,不需要加类限定
。
友元函数可以访问类中的私有成员和其他数据,但是访问不可直接使用数据成员,需要通过对对象进行引用
。
友元函数在调用上同一般函数一样
,不必通过对对象进行引用。
#include
#include
using namespace std;
class persion{
public:
persion(char *pn);
//友元函数;
friend void setweigth(persion &p,int h);//注意,参数列表中一般会有一个引用类型的形参,原因参考上面的使用要点3和4;
void disp(); //类成员函数
private:
char name[20];
int weigth,age;
};
persion::persion(char *pn) //构造函数
{
strcpy(name,pn);
weigth=0;
}
void persion::disp()
{
cout<<name<<"--"<<weigth<<endl;
}
//友元函数的具体实现:这里没有类限定例如 (perion::setweigth)这种形式,这里可以与上面的disp()做个对比,一个属于类的成员,有限定,不属于类的成员函数,没有加限定。
void setweigth(persion &pn,int w)
{
strcpy(pn.name,pn);//实现字符串复制
pn.weigth=w; //私有成员数据赋值
}
void main()
{
persion p("zhansan");
//调用实现setweigth(),与一般函数调用一致。
setweigth(p,60);
p.disp(); //调用类的成员函数。
}
更为详细的解释
当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;
回调函数就相当于一个中断处理函数
,由系统在符合你设定的条件时自动调用
。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件(中间函数),就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用
;
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数
,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。
在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做 触发回调事件,店员给你打电话叫做 调用回调函数,你到店里去取货叫做 响应回调事件。
因为可以把调用者和被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
更加安全;
更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图
貌似就用过这两种
void * memcpy ( void * destination, const void * source, size_t num );
char *strncpy(char *destinin, char *source, int maxlen) ;
int sprintf( char *buffer, const char *format [, argument,...] );
sprintf函数的功能与printf函数的功能基本一样,只是它把结果输出到指定的字符串中了
sprintf(s,"%s%d%c",“test”,1,‘2’);
a) string转const char*
string s = “abc”;
const char* c_s = s.c_str();
b) const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);
c) string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());
d) char* 转string
char* c = “abc”;
string s(c);
e) const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);
f) char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
赋值初始化
,通过在函数体内进行赋值初始化;列表初始化
,在冒号后使用初始化列表进行初始化。
这两种方式的主要区别在于:
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的
。
列表初始化是给数据成员分配内存空间时就进行初始化
,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。
class Animal
{
public:
Animal(int weight,int height): //A初始化列表
m_weight(weight),
m_height(height)
{
}
Animal(int weight,int height) //B函数体内初始化
{
m_weight = weight;
m_height = height;
}
private:
int m_weight;
int m_height;
};
① 当初始化一个引用成员时;
② 当初始化一个常量成员时;
③ 当调用一个基类的构造函数,而它拥有一组参数时;(或者说子类初始化父类的成员)
④ 当调用一个成员类的构造函数,而它拥有一组参数时;(或者说当数据成员是对象的时候)
代码举例
BOOL IsBigEndian()
{
int a = 0x1234;
char b = *(char *)&a; //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分
if( b == 0x12)
{
return TRUE;
}
return FALSE;