基础部分整理
左值右值
左值与右值这两概念是从 c 中传承而来的,在 c 中,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式).
- 待补充...
static
-
全局静态变量
在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量.
-
内存中的位置:静态存储区,在整个程序运行期间一直存在。
-
初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他被显式初始化);
-
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
-
-
局部静态变量
在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。-
**内存中的位置: **静态存储区
-
初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他被显式初始化);
-
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
-
-
静态函数
在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都是可以extern(外部变量)的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。函数的实现使用 static 修饰,那么这个函数只可在本 cpp 内使用,不会同其他 cpp 中的同名函数引起冲突;
warning:不要在头文件中声明 static 的全局函数,不要在 cpp 内声明非 static 的全局函数,如果你要在多个 cpp 中复用该函数,就把它的声明提到头文件里去,否则 cpp 内部声明需加上 static 修饰; -
类的静态成员变量
静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。类的静态成员变量需要在类外分配内存空间。
-
类的静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用可以用类名。在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
举例如下:
#include <bits/stdc++.h>
using namespace std;
class test {
public:
static int m_value; //定义私有类的静态成员变量
public:
test() {
m_value++;
}
static int getValue() //定义类的静态成员函数
{
return m_value;
}
};
int test::m_value = 0; //类的静态成员变量需要在类外分配内存空间
int main() {
test t3;
cout<<t3.m_value<<endl;
cout << "test::m_value = " << test::m_value << endl;
//通过类名直接调用公有静态成员变量,获取对象个数
cout << "t3.m_value = " << t3.m_value << endl;
//通过对象名调用公有静态成员变量,获取对象个数
cout << "test::getValue() = " << test::getValue() << endl;
//通过类名直接调用公有静态成员函数,获取对象个数
cout << "t3.getValue() = " << t3.getValue() << endl;
//通过对象名调用静态成员函数获取对象个数
return 0;
}
/*
test::m_value = 1
t3.m_value = 1
test::getvalue() = 1
t3.getValue() = 1
*/
this
- 作用就是指向成员函数所作用的对象,所以非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。
- 静态成员函数中是不能使用this指针,因为静态成员函数相当于是共享的变量,不属于某个对象的变量。
const
参考链接:https://blog.csdn.net/csdn_chai/article/details/78041050
具体用法
用法 | 代码 | 作用 |
---|---|---|
const 变量 | const int a; | 不能修改值,必须初始化 |
const 类对象 | const MyClass a; | 不能修改成员变量的值,不能调用非 const 函数 |
指向 const 变 量的指针 | const int * a; | 指向内容不可变,指向可变 |
const 指针 | int * const a; | 指向内容可变,指向不可变 |
指向 const 变量的 const 指针 | const int * const a; | 指向内容不可变,指向也不可变。const 引用 |
const 变量作为函数参数 | void myfun(const int a); | 函数内部不能改变此参数。指向 const 变量的指针做参数,允许上层用一般指针调用。(反之不可) |
const 返回值 | const string& myfun(void); | 用于返回const引用,上层不能使用返回的引用来修改对象 |
const 成员变量 | const int a; static const int a; | 必须在初始化列表初始化,之后不能改变。static const成员变量需要单独定义和初始化 |
const 成员函数 | void myfun(void) const; | this指针为指向const对象的const指针。不能修改非mutable 的成员变量 |
注意:
- const成员方法本质上是使得this指针是指向const对象的指针,所以在const方法内,const 成员函数可以被非const和const对象调用,而const对象只能调用const 成员函数。原因得从C++底层找,C++方法调用时,会传一个隐形的this参数(本质上是对象的地址,形参名为this)进去,所有成员方法的第一个参数是this隐形指针。const成员函数的this指针是指向const对象的const指针,当非const对象调用const方法时,实参this指针的类型是非const对象的const指针,赋给const对象的const指针没有问题;但是如果const对象调用非const方法,此时实参this指针是指向const对象的const指针,无法赋给非const对象的const指针,所以无法调用。注意this实参是放在ecx寄存器中,而不是压入栈中,这是this的特殊之处。在类的非成员函数中如果要用到类的成员变量,就可以通过访问ecx寄存器来得到指向对象的this指针,然后再通过this指针加上成员变量的偏移量来找到相应的成员变量。
- const 指针、指向const的指针和指向const的const指针,涉及到const的特性“const左效、最左右效”
- const 全局变量有内部链接性,即不同的文件可以定义不同的同名const全局变量,使用extern定义可以消除内部链接性,称为类似全局变量,如extern const int a = 10.另一个文件使用extern const int a; 来引用。而且编译器会在编译时,将const变量替换为它的值,类似define那样。
const 常量和define的区别
- const常量有数据类型,而宏定义没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意想不到的错误(边际效应)。
- 有些集成化的调试工具可以对const常量进行调试,但是不能对宏定义进行调试。
- 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
- 内存空间的分配上。define进行宏定义的时候,不会分配内存空间,编译时会在main函数里进行替换,只是单纯的替换,不会进行任何检查,比如类型,语句结构等,即宏定义常量只是纯粹的置放关系,如#define null 0;编译器在遇到null时总是用0代替null它没有数据类型,而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查,所以const与define之间的区别在于const定义常量排除了程序之间的不安全性。
- const常量存在于程序的数据段,#define常量存在于程序的代码段
- const常量存在“常量折叠”,在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表,可以算作一种编译优化。因为编译器在优化的过程中,会把碰见的const全部以内容替换掉,类似宏。
extern(外部变量)
转自:http://blog.csdn.net/xingjiarong/article/details/47656339
利用关键字extern,可以在一个文件中引用另一个文件中定义的变量或者函数。
一、引用同一个文件中的变量
#include<stdio.h>
int func();
int main()
{
func(); //1
printf("%d",num); //2
return 0;
}
int num = 3;
int func()
{
printf("%d
",num);
}
如果按照这个顺序,变量 num在main函数的后边进行声明和初始化的话,那么在main函数中是不能直接引用num这个变量的,因为当编译器编译到这一句话的时候,找不到num这个变量的声明,但是在func函数中是可以正常使用,因为func对num的调用是发生在num的声明和初始化之后。
如果我不想改变num的声明的位置,但是想在main函数中直接使用num这个变量,怎么办呢?可以使用extern这个关键字。像下面这一段代码,利用extern关键字先声明一下num变量,告诉编译器num这个变量是存在的,但是不是在这之前声明的,你到别的地方找找吧,果然,这样就可以顺利通过编译啦。但是你要是想欺骗编译器也是不行的,比如你声明了extern int num;但是在后面却没有真正的给出num变量的声明,那么编译器去别的地方找了,但是没找到还是不行的。
下面的程序就是利用extern关键字,使用在后边定义的变量。
#include<stdio.h>
int func();
int main()
{
func(); //1
extern int num;
printf("%d",num); //2
return 0;
}
int num = 3;
int func()
{
printf("%d
",num);
}
如果extern这个关键字就这点功能,那么这个关键字就显得多余了,因为上边的程序可以通过将num变量在main函数的上边声明,使得在main函数中也可以使用。
extern这个关键字的真正的作用是引用不在同一个文件中的变量或者函数。
main.c
#include<stdio.h>
int main()
{
extern int num;
printf("%d",num);
return 0;
}
b.c
#include<stdio.h>
int num = 5;
void func()
{
printf("fun in a.c");
}
例如,这里b.c中定义了一个变量num,如果main.c中想要引用这个变量,那么可以使用extern这个关键字,注意这里能成功引用的原因是,num这个关键字在b.c中是一个全局变量,也就是说只有当一个变量是一个全局变量时,extern变量才会起作用,下面这样是不行的。
main.c
#include<stdio.h>
int main()
{
extern int num;
printf("%d",num);
return 0;
}
b.c
#include<stdio.h>
void func()
{
int num = 5;
printf("fun in a.c");
}
另外,extern关键字只需要指明类型和变量名就行了,不能再重新赋值,初始化需要在原文件所在处进行,如果不进行初始化的话,全局变量会被编译器自动初始化为0。像这种写法是不行的。
extern int num=4;
但是在声明之后就可以使用变量名进行修改了,像这样:
#include<stdio.h>
int main()
{
extern int num;
num=1;
printf("%d",num);
return 0;
}
如果不想这个变量被修改可以使用const关键字进行修饰,写法如下:
mian.c
#include<stdio.h>
int main()
{
extern const int num;
printf("%d",num);
return 0;
}
b.c
#include<stdio.h>
const int num=5;
void func()
{
printf("fun in a.c");
}
使用include将另一个文件全部包含进去可以引用另一个文件中的变量,但是这样做的结果就是,被包含的文件中的所有的变量和方法都可以被这个文件使用,这样就变得不安全,如果只是希望一个文件使用另一个文件中的某个变量还是使用extern关键字更好。
三、引用另一个文件中的函数
extern除了引用另一个文件中的变量外,还可以引用另一个文件中的函数,引用方法和引用变量相似。
mian.c
#include<stdio.h>
int main()
{
extern void func();
func();
return 0;
}
b.c
#include<stdio.h>
const int num=5;
void func()
{
printf("fun in a.c");
}
这里main函数中引用了b.c中的函数func。因为所有的函数都是全局的,所以对函数的extern用法和对全局变量的修饰基本相同,需要注意的就是,需要指明返回值的类型和参数。
new和malloc
转自:https://www.cnblogs.com/shilinnpu/p/8945637.html
new 是运算符,malloc是库函数
先放一个表格
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配成功返回值 | 完整类型指针 | void* (无类型指针) |
内存分配失败返回值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
1.申请内存所在的位置
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
2.返回类型安全性
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*(表示空的指针类型的变量)指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了。
3.内存分配失败时的返回值
new内存分配失败时,会抛出bad_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
4.是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
class A{...}
A * ptr = new A;
A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);
5.是否调用析构函数/构造函数
使用new操作符来分配对象内存时会经历三个步骤:
- 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
- 第三部:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
- 第一步:调用对象的析构函数。
- 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。
6.对数组的处理
C++提供了new[]与delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象
使用new[]分配的内存必须使用delete[]进行释放:
delete [] ptr;
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并不知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,再给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:
int * ptr = (int *) malloc( sizeof(int)*10);//分配一个10个int元素的数组
7.new和malloc是否可以相互调用
operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。下面是编写operator new /operator delete 的一种简单方式,其他版本也与之类似:
void * operator new (sieze_t size)
{
if(void * mem = malloc(size)
return mem;
else
throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
free(mem);
}
8.是否可以被重载
opeartor new /operator delete可以被重载,而malloc/free并不允许重载。
9.能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
new没有这样直观的配套设施来扩充内存。
10. 客户处理内存分配不足
在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:
namespace std
{
typedef void (*new_handler)();
}
指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:
namespace std
{
new_handler set_new_handler(new_handler p ) throw();
}
set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。
对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。
指针
指针与引用的区别?
-
指针是一个变量,它的内容是所指的内存地址,而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
int a=1;int &b=a;
一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
-
指针需要解引用才能访问对象,引用不需要
-
指针可以不初始化,引用在定义时必须初始化,且以后不可转移引用的对象。
-
指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
-
指针可以有多级,但是引用只能是一级(int **p合法但是int &&a是不合法的)
-
指针有const指针,即int* const ptr;而引用没有const,即int& const a 没有;
-
指针变量需要分配栈空间;而引用不需要,仅仅是个别名
-
sizeof(指针)得到指针大小;sizeof(引用)得到对应对象的大小
-
指针加法和引用加法不一样
-
引用不需要释放内存空间,在编译时就会优化掉
指针与数组名的区别?
- 数组名不是指针,对数组名取地址,得到整个数组的地址
- 数组名 + 1会跳过整个数组的大小,指针+1只会跳过一个元素的大小
- 数组名作为函数参数传递时,退化为指针
- sizeof(数组名)返回整个数组的大小,sizeof(指针)返回指针大小
- 数组名无法修改值,是常量
- int (*p)[] = &arr; 才是正确的数组指针写法
野指针、空指针的概念?
- 访问一个已销毁或者访问受限的内存区域的指针(无效内存),野指针不能判断是否为NULL来避免,不能对野指针取内容。
- 空指针是指置为0NULL ullptr的指针,可以对空指针delete多次。
内存泄露和内存溢出
-
内存泄漏就是内存申请后,用完没有释放,造成可用内存越来越少。
-
内存溢出指用户实际的数据长度超过了申请的内存空间大小,导致覆盖了其他正常数据,容易造成程序异常,严重的,攻击者可以以此获取程序控制权。
函数模板
用来解决传参的变量类型的问题
template <class 类型参数1,class 类型参数2,...>
//template <typename 类型参数1,typename 类型参数2,...>
返回值类型 模板名 (形参表)
{
函数体
};
- 传入多个参数举例
template <class T1, class T2>
T2 MyFun(T1 arg1, T2 arg2)
{
cout<< arg1 << " "<< arg2<<endl;
return arg2;
}
T1 是传入的第一种任意变量类型,T2 是传入的第二种任意变量类型。
class和struct
如果没有多态和虚拟继承,在C++中,struct和class的存取效率完全相同,存取class的数据成员与非虚函数效率和struct完全相同,不管该数据成员是定义在基类还是派生类。
- class的数据成员在内存中的布局不一定是数据成员的声明顺序,C++只保证处于同一个access section的数据成员按照声明顺序排列
在C++中,class和struct做类型定义是只有两点区别:
- 默认继承和访问权限不同,class继承默认和访问是private,而struct默认继承和访问是public
- class还可用于定义模板函数参数,像typename,但是关键字struct不能同于定义模板h函数参数
C++保留struct关键字,原因
- 保证与C语言的向下兼容性,C++必须提供一个struct
- C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制
- 对struct定义的扩展使C语言的代码能够更容易的被移植到C++中
数组和链表
1、存储空间上:数组在内存中是连续的,从栈中分配空间;链表是可以不连续的,从堆中分配空间。
2、在查询,访问方式上:数组可以随机访问其中的元素,查找速度相对较快,链表则必须是顺序访问,不能随机访问。
3、空间的使用上:链表对内存空间的利用率较高,可扩展性高;数组则不能,数组的空间大小是固定的,不适合动态存储,不方便动态添加。
4、添加或删除元素时,数组比链表慢,因为数组要移动大量的元素,而链表只需修改指针即可。
堆和栈的区别
1、申请方式:栈:由系统自动分配。 堆:需要程序员自己申请,并指明大小。
2、申请效率的比较:栈:由系统自动分配,速度较快。
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
3、申请大小的限制:栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M,超出剩余空间会溢出。
堆的大小受限于计算机系统中有效的虚拟内存。
4、数据结构区别:堆可以被看成是一棵树,如:堆排序。栈:一种先进后出的数据结构。
5、缓存方式区别:栈使用的是一级缓存,它们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定。
浅拷贝和深拷贝
转自:https://www.cnblogs.com/mikeCao/p/8710837.html
- 深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:
如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
友元
https://blog.csdn.net/weixin_42513339/article/details/81101644