“声明”:意味着告诉编译器关于变量名称、变量类型、变量大小、函数名称、结构名称、大小等等信息,并且在声明阶段不会给变量分配任何的内存。
“定义”:定义就是在变量声明后,给它分配上内存。可以看成“定义 = 声明 + 内存分配”。
在C++中,内存分为5个区:堆、栈、自由存储区、全局/静态存储区、常量存储区
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
1)全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
2)静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
3)局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
4)静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。
静态变量会被放在程序的静态数据存储区(数据段)(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
把局部变量改变为静态局部变量后,是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。
1)预处理:预处理的过程主要处理那些源代码文件中只能够以“#”开始的预处理指令。
2)编译:编译就是将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生相应的汇编代码文件。
3)汇编:汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程,即生成目标文件。
4)链接:链接就是把每个源代码独立的编译,然后按照它们的要求将它们组装起来,链接主要解决的是源代码之间的相互依赖问题。
静态链接:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应的链接方式称为静态链接。静态库的缺点在于:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
动态链接:动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
死锁问题被认为是线程/进程间切换消耗系统性能的一种极端情况。在死锁时,线程/进程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是任务永远无法执行完成。
打个比方,假设有P1和P2两个进程,都需要A和B两个资源,现在P1持有A等待B资源,而P2持有B等待A资源,两个都等待另一个资源而不肯释放资源,就这样无限等待中,这就形成死锁,这也是死锁的一种情况。给死锁下个定义,如果一组进程中每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程是死锁的。
当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:某资源只能被一个进程使用,其他进程请求该资源时,只能等待,知道资源使用完毕后释放资源。
2.请求和保持条件:程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{
// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
加入语句
Student::Student(const Student &s)
{
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
} //拷贝构造函数,const防止对象被改变
执行结果:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。3.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
数组在分配内存的时候是一块连续的空间,并且每个元素的内存是一样的,因此可以用下标快速访问;但正因为如此,在其中插入或者删除的操作就比较麻烦,要移动别的元素的位置,因此需要快速访问存取并且不频繁增删就用数组。
链表的每个元素使用指针相互链接,分配的空间比较自由,每个元素可以不同类型不同大小,但是访问就必须链式线扫且没有下标,插入删除比较方便,只用替换和删除指针即可,适合频繁增删的操作需求。
A * ptr = new A;
A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序是稳定的。
冒泡排序:
1、对数组array[n]进行从0n-1项的扫描,每碰到相邻两项数值大的在前小的在后时,对二者进行交换。当扫描进行完成后,0n-1中最大的元素必然已经在array[n-1]就位,而所有数值较小,序号却靠后的元素,序号也减小了1。
2、既然最大的元素已在array[n-1]的位置就位,接下来的扫描只需从0n-2。具体过程同1。同样的,扫描结束后0n-2中最大的元素(全数组第二大的元素)必然已经在array[n-2]就位,而所有数值较小,序号却靠后的元素,序号也减小了1。
3、如此不断重复,直到最小的元素在array[0]的位置就位。
从上述描述中我们可以看到“冒泡排序”这个名字的由来:每一次扫描,都可以使得数值较小,序号却靠后的元素的序号减少1,宏观来看这些元素就像是从数组底部向上慢慢上浮的泡泡。
void BubbleSort(int a[],long size)
{
for(long i=0;i<size;i++)
{
bool flag=true;
for(long j=0;j<size-i-1;j++)
{
if(a[j]>a[j+1])
{
swap(a[j+1],a[j]);
flag=false;
}
}
if(true==flag)return;
}
}
插入排序:
插入排序和人们打牌时所用的排序方式类似:
1、抽第一张牌,此时手上的牌只有一张,所以是有序的。
2、再抽一张牌,和手上的那张牌的大小进行比较,比它大就放在后面,否则放在前面。
3、再抽一张牌,和手上的牌进行比较,插入在合适的位置,保持手上的牌有序。
4、不断重复3,直到牌抽完。
从宏观来看,插入排序把数组分割成两部分,前段有序后段无序,随着插入排序的进行,后段无序的牌也越来越少,直到后段全部融入前段,排序也就结束了。
void InsertSort(int arr[],long size)
{
for(int i=1;i<size;i++)
{
int temp=arr[i];
int j=i-1;
while(j>=0)
{
if(arr[j]>temp)arr[j+1]=arr[j];
else break;
j--;
}
arr[j+1]=temp;
}
}
选择排序:
1、从数组中找出最小的元素,找出来后,将其和数组首元素互换位置。
2、此时待排序的数组规模-1。对这个规模减小的数组进行步骤1、直到待排序的数组规模为0。
void SelectSort(int arr[],long size)
{
for(long i=size-1;i>=0;i--)
{
int maxnum=arr[i];
long rank=i;
for(long j=0;j<i;j++)
{
if(arr[j]>=maxnum)
{
maxnum=arr[j];
rank=j;
}
}
swap(arr[rank], arr[i]);
}
}
快速排序:
1、从数组中随机选出一个元素,和数组首元素互换位置,并记下其大小。
2、使用两个指针,指针a指向数组头,指针b指向数组尾。
3、指针b从后向前扫描,找到一个数比选出元素小时,暂停,将其值保存在指针a所指的位置中。
4、指针a从前往后扫描,找到一个数比选出元素大时,暂停,将其值保存在指针b所指的位置中。
5、循环重复3、4,直到a 6、将选出元素放在指针a、b同时指向的位置,并用此位置将数组分割成前后不包含选出元素的两个子数组,对子数组进行步骤1~6。
void QuickSort(int arr[],long lo,long hi)
{
if(hi-lo<2)return;
long rank=rand()%(hi-lo)+lo,ranklo=lo,rankhi=hi;
swap(arr[ranklo],arr[rank]);
int key=arr[lo];
hi--;
while(lo<hi)
{
while(lo<hi&&key<=arr[hi])hi--;
arr[lo]=arr[hi];
while(lo<hi&&arr[lo]<=key)lo++;
arr[hi]=arr[lo];
}
arr[lo]=key;
QuickSort(arr, ranklo,lo);
QuickSort(arr, lo+1,rankhi);
}
归并排序:
归并排序的操作有两步,分割和归并.
1、分割:将数组二等分,并将得到的子数组继续二等分,直到每个子数组只剩下一个元素为止。
2、归并:不断将原本属于同一个数组的两个子数组归并成一个有序的数组,方法为不断比较子数组的首元素,并弹出较小的放入合并后组成的数组中。直到所有子数组合并为一个数组。
void MergeSort(int arr[],long lo,long hi)
{
if(hi-lo<2)return;
long mi=(lo+hi)/2;
MergeSort(arr,lo, mi);
MergeSort(arr,mi, hi);
Merge(arr,lo,mi,hi);
}
void Merge(int arr[],long lo,long mi,long hi)
{
long ml=mi-lo;
int *b=new int[ml];
for(long i=0;i<ml;i++)b[i]=arr[lo+i];
//print(b, ml);
long x=0,y=mi;
for(long i=lo;i<hi;)
{
if((b[x]<=arr[y]&&x<ml)||y>=hi)
{
arr[i++]=b[x++];
}
if((arr[y]<b[x]&&y<hi)||x>=ml)
{
arr[i++]=arr[y++];
}
}
delete []b;
}