C++ 基础
new/delete malloc/free
#include<iostream>
using namespace std;
/*
new和malloc区别是啥
delete和free区别是啥
malloc 和 free 称为c语言的库函数
new 和 delete 成为C++的运算符
new不仅可以做内存开辟,还可以做内存初始化操作,而new开辟内存失败,是通过抛出bad_alloc类型的异常来判断
malloc只能做内存开辟,开辟失败是通过返回值和nullptr作比较
*/
int main()
{
int* p = (int*)malloc(sizeof(int)); //在内存中开辟一个整形大小的内存
//利用空指针判断内存是否开辟成功 malloc按照字节开辟内存 返回一个void* 需要(int*)强转
if (p == nullptr)
{
return -1;
}
//只开辟内存 不初始化 使用free函数 把起始地址传入即可
*p = 20;
free(p);
try
{
int* p1 = new int(20); //开辟一个指定类型内存 使用小括号初始化 new指定类型,所以返回值不需要强制转换
}
catch (const bad_alloc & e)
{
}
delete p;
int* q = (int*)malloc(sizeof(int) * 20);
if (q == nullptr)
{
return -1;
}
free(p);
int* q1 = new int[20]; //中括号 代表开辟的数量多少 小括号代表初始化
int* q1 = new int[20]();//不加小括号不初始化,加了小括号全部初始化为0
delete[]q1;
int* p1 = new int(20);
int* p2 = new (nothrow) int; //不抛出异常版本的new
const int* p3 = new const int(40); //在堆上开辟了一个常量内存 初始化为40 常量的地址不能给一个普通的指针
//定位new 容器的空间配置器
int data = 0;
int* p4 = new(&data) int(50); //在指定的内存上 划分出四字节大小的内存 初值给为50
return 0;
}
引用和指针
引用基础演示
#include<iostream>
#define CONST 10
using namespace std;
/*
C++的引用 引用和指针的区别?
1.左值引用和右值引用
右值引用
1.int &&c = 20;专门引用右值,不能引用左值,指令上自动产生临时量,然后直接引用临时量
2.右值引用本身是一个左值,只能用左值引用来引用它、
3.不能用一个右值引用变量来引用一个左值
2.引用的实例
3.在指令层面是没有引用和汇编的区分的,都是通过地址/指针的操作来进行的 引用的处理在底层都是通过指针来进行的
引用是一种更安全的指针
1.引用是必须初始化的 指针是可以不初始化的
2.引用只有一级引用没有多级引用,指针可以有一级指针也可以有多级指针
3.定义一个引用变量,和定义一个指针变量,其汇编指令是一摸一样的
4.通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向内存的值,其底层指令也是一样的 底层不区分
*/
void swap(int *a, int *b)
{
/*
操作*a *b就是操作实参a,b 因为传入的是实参
形参到实参是按值传递 形参只在函数内部生效 在函数结束后释放 不产生作用
为了处理 传入地址就可以解决该问题
*/
int temp = *a;
*a = *b;
*b = temp;
}
void swap(int& x, int& y)
{
/*
传入的还是地址 和上文一致
引用方便了很多
*/
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10;
int* p = &a; //指针指向a内存 指针需要单独的内存 所以需要存放a的地址
int& b = a; //引用变量来引用a内存 直接指定引用的内存即可 不能指定一个引用变量而不给它指定一个被引用的变量
//并且初始化的值一定可以取地址 因为本质是指针 指令是拿出右边的地址放在底层左边的指针内存中
//int& c = 20; 初始化的值一定可以取地址 生成的时候是把右边引用的地址拿出来赋值给底层的一个指针 20无法取地址,引用变量指令无法生成
//引用和指针定义在汇编代码是完全相同的
/*
lea eax , [a]
mov dword ptr[b] , eax
*/
*p = 20; //可以通过指针来修改指向内存的值
cout << a << " " << *p << " " << b << endl;
b = 30; //通过b来修改它所引用的内存的值
//通过底层的地址 拿出来地址解引用赋值
cout << a << " " << *p << " " << b << endl;
//引用和指针在修改内存的值的时候 在汇编代码上也是完全一样的
/*
*/
//说明 a/*p/b 属于同一块内存
a = 15;
int c = 20;
swap(&a, &c);
cout << a << " " << c << endl;
return 0;
}
使用引用来引用一个数组的本质
#include<iostream>
using namespace std;
int main()
{
int array[5] = {};
int* p = array;
//定义一个引用变量,来引用array数组
int(*qb)[5] = &array;
//难的引用问题可以先写指针 然后引用
int(&q)[5] = array;
cout << sizeof(array) << endl;
//大小是整个数组的大小 长度为5 每个元素占4个字节
cout << sizeof(p) << endl;
//32位操作系统 指针的大小都是四字节
cout << sizeof(qb) << endl;
//引用是它引用对象的别名,使用引用变量往往会做解引用操作
cout << typeid(array).name() << endl;
cout << typeid(p).name() << endl;
cout << typeid(q).name() << endl;
return 0;
}
左值引用和右值引用
#include<iostream>
using namespace std;
int main()
{
int a = 10; //左值,他有内存,有名字,值可以修改
int& b = a;
/*
lea 将一个内存地址直接赋值给目的操作数
mov 将一个内存地址中的值直接赋值给目的操作数
左值引用汇编代码
lea eax,[a] 把a的内存地址放到寄存器
mov dword ptr[b], eax 把寄存器中的值,放到指针的内存空间中
*/
//int &c = 20; /右值,他没内存,没名字
//C++11提供右值引用 可以提高效率
int&& c = 20; //虽然指令和const int & b一样但是没有const修饰 所以c的值可以改变 不是左值
/*
右值引用汇编代码
mov dword ptr[ebp-30], 14h 将14h放置到一个栈上开辟的临时量中
lea eax, [ebp-30] 把临时量的地址放到eax寄存器中
mov dword ptr[c],eax 然后把地址放到一个底层指针变量中
*/
//int& b = 20; //错误,因为20不能取地址
const int& b = 20; //正确 但是由于const修饰 所以b作为左值
/*
int temp = 20;
temp -> b
把临时量地址存储到底层内存当中
const 和右值引用得到的 指令基本相同
*/
//一个右值引用变量本身使一个左值 有内存 有名字
int&& e = c; //错误 一个右值引用必须引用一个右值 不能引用左值 左值有内存,生成临时量没必要
//右值引用就是来引用右值的,不能引用左值
return 0;
}
const&一级指针&引用
#include<iostream>
using namespace std;
/*
const 一级指针,引用的结合
*/
int main()
{
const int& a = 20; //加const后,左值引用会自动在指令中生成一个临时变量,可以使右值可以赋值
//写一句代码,在内存0x0018ff44处写一个4字节的10
int* p = (int*)0x0018ff44;
*p = 10;
//右边是一个地址(常量)不能取地址,没内存,没地址,不能修改,所以是右值,要采用右值引用&&
int*&& p = (int*)0x0018ff44;
//非常量引用的初始值必须为左值
//const int*& p = (int*)0x0018ff44;
//常量引用的初始值可以为右值
int* const& p = (int*)0x0018ff14;
int aa = 10;
int* pp = &aa;
int** q = &pp;
//使用取地址符覆盖指针 使其变为一个引用变量
int*& q = pp;
//const int*& q = &p; 非常量引用的初始值必须是左值
//判断是否正确的方法,将引用还原成指针 判断指针的正确性
//const int** q = &p; 错误 故引用也错误
int aaa = 10;
const int bb = 20;
//int* const p = &bb;
int* const p = &aaa;
int*& q = p;
int** q = &p;
return 0;
}
#.# const指针和引用的结合
const的用法
#include<iostream>
using namespace std;
/*
const 指针 引用 在函数中的引用
const修饰的变量不能够再作为左值!!!初始化完成后值不能够被修改!!!
const 怎么理解? C/C++中 const的区别是什么
在C中 可以给const只声明 不初始化但这样做 后面就没有初始化const修饰的变量的机会 不叫常量叫常变量
不能当作常量来使用比如 int array[a] 如果a是常变量 就会报错
a不能作为左值 但是a这个量占用的内存可以被修改 语法不能修改 但是值可以修改 插入汇编指令也可
在C++中,可以必须给const初始化,叫常量不叫常变量
可以定义数组的大小,可以作为数组的下标,
如果初始化的时候没有给常量一个立即数,常量会退化为常变量,因为初始值不是一个立即数
【注意】const编译方式, C中const当作一个变量来编译生成指令。
C++中,所有出现const常量名字的地方,都被常量初始化替换了。
*/
int main()
{
int a = 10;
a = 20;
//变量可以作为左值
const int b = 20;
//const 修饰变量后 变量就不能作为左值
//b = 30;
//C++编译器会优化 *(&a)为常量20
//const 在编译阶段就初始化了 所以改了const int a 的内存的值 但是打印的不是const内存的值 而是编译时出现的初值
return 0;
}
int main02()
{
const int b = 20;
//按照C的处理方式
int* p = (int*)&b; //强转a的类型
*p = 30;
}
const和一级指针结合的应用
#include<iostream>
using namespace std;
/*
const 和 一级指针的结合
const 和 二级指针的结合(多级指针)
C++ 常量和普通变量的两种区别
1.编译方式不同 所有出现常量名字低地方都会用常量的初值来替换
2.不能作为左值
*/
/*
const 修饰的量常出现的的错误是
1.常量不能再作为左值 == 直接修改常量的值
2.不能把常量的地址泄露给一个普通的指针或者普通的引用变量 == 间接修改常量的值
*/
int main()
{
const int a = 10;
int b = 20;
// 不能用普通指针指向常量
// int* p = &a; int* == const int * *地址 学名指针 普通指针解引用赋值就会修改常量的值
//把常量的地址泄露给普通的指针 指针解引用完全可以随意赋值 可以通过指针修改常量的值
//编译器要杜绝 上述两种常量的错误
// const和一级指针的结合
// const和一级指针的结合有两种情况
// C++语言规范 const修饰的是离他最近的类型
const int *a; //*p = 20; p = &a 指针的指向是常量
//const修饰的表达式不能再被赋值 距离const最近的类型是int 就代表const修饰的表达式是*p(指针的指向) *p就不能被赋值
//可以通过指针指向一块内存但是不同通过指针解引用修改一块内存的值 p本身没有被修饰,所以可以更改p的指向
//可以任意指向不同int类型的内存,但是不能通过指针 间接修改指向的内存的值
int const *b; //和上面等价 *不可能单独作为类型 所以距离const最近的还是int类型
int* const c = &b; // p = &b; 指针是常量
//初始化赋值指向一块内存 就不能初始化指向其他内存 可以通过指针解引用来修改这块内存 因为*p没有被const修饰
//距离const最近的类型 是int* *不可能单独作为类型 只能向前继续扩展 成为int* const修饰p本身 p本身成为了常量所以p不能再被修改
//不能指向其他内存,但是可以通过指针解引用修改指向内存的值
const int* const d = &b; //*p = &b p=b
//第一个const 修饰 *d表明d指向的内存区域 *d就不能被赋值
//第二个const 修饰d 表明d指向的位置不能改变
return 0;
}
#include<iostream>
using namespace std;
/*
总结const和指针的类型转换公式:
const int * = int * 正确的
int * = const int * 错误的
*/
int main()
{
//陷阱
int a1 = 10;
const int* pa = &a1;
//int* q = pa;
/*
NULL = 0 不区分指针和整数 nullptr空指针
const如果右边没有指针* const是不参与类型的 const表示右边是个常量 不可以用作为左值
*/
int* q1 = nullptr;
int* const q2 = nullptr;
cout << typeid(q1).name() << endl;
cout << typeid(q2).name() << endl;
int a = 10;
int* p1 = &a; //整形是int 取地址是* &a就是int*型
const int* p2 = &a; //const int * = int *
int* const p3 = &a; //int* = int *
int* p4 = p3; //int* = int *
return 0;
}
const和二级指针结合的应用
#include<iostream>
using namespace std;
/*
const和二级指针的结合
int ** <= const int** 错误
const int** <= int** 错误
int ** <= int * const * 错误
int * const * <= int ** 正确
*/
int main()
{
int a = 10;
int* p = &a;
int** q = &p; //q存放p的地址 *q存放p的值(p中存储的地址) **q存放p的值(p存放的地址)存放的值
cout << q << "+"<< *q <<"+" << **q << endl;
cout << p <<"+"<< *p << endl;
// const int** q; //const修饰的最近的类型是int 所以const修饰的表达式是**q **q不能被修改
// int* const* q; //const修饰的最近的类型是int* 所以const修饰的表达式是*q
// int** const q = &p; //const修饰的最近的类型是int** 所以const修饰的表达式是q
int aa = 10;
int* pp = &a;
// const int** qq = &pp; //const int*(类型) *p(一级指针)2l
//把常量地址 赋值给指针
/*
const int* *q = & p
*q <=> p
const int b = 20;
*q = &b;
*/
return 0;
}
二级指针和const结合常见错误分析
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int* p = &a;
//const int ** = int **
const int** q = &p;
const int b = 20;
const int* pb = &b;
*q = pb;
return 0;
}
#include<iostream>
using namespace std;
//const 如果右边没有指针 const不参与类型
//const 本质 只去使用它 不去修改它
/*
int * <= const int * 错误
const int * <= int * 正确
int ** <= const int ** 错误
const int ** <= int ** 错误
int ** <= int* const* 错误
int* const* <= int ** 正确
*/
int main()
{
int a = 10;
int b = 20;
const int* p = &a; //const int* <= int * const和int结合 修饰*p p中的值不能修改 但是p的指向可以修改
p = &b;
//int* q = p; //int* <= const int * 错误
int* p = &a;
const int** q = &p; //const int ** <=> int ** 都是错的
return 0;
}
函数重载 inline函数 参数带默认值的函数
参数带默认值的函数
#include<iostream>
using namespace std;
/*
问题1:默认值为什么从右向左给
问题2:调用效率的问题 立即数入栈比变量快一倍
问题3:声明的时候给函数形参默认值行不行 定义可以给默认值 声明也可以
问题4:形参给默认值的时候,不管是定义处给,还是声明处给,形参默认值只能出现一次
问题5:形参给默认值的叠加问题,右边先给,下一个声明处左边给,照样可以成功
int sum(int a, int b = 20);
int sum(int a = 10; int b);
*/
//形参带默认值的函数
//OOP成员方法,尤其是构造函数 经常使用
//从指令上 观察是否添加默认值的区别
//给默认值 只能从右向左给 不然会报错 sum(,20)没有这样的语法
//给参数从右向左给 压栈从左向右压
int sum(int a= 10, int b = 20)
{
return a + b;
}
int main()
{
int a = 14;
int b = 26;
int ret = sum(a, b);
/*
mov eax, dword ptr[ebp-8] //从变量内存中取值放到寄存器
push eax //push入栈
mov ecx, dword ptr[ebp-4]
push ecx
call sum
*/
cout << "ret:" << ret << endl;
ret = sum(a);
/*
push 14H
mov ecx, dword ptr[ebp-4]
push ecx
call sum
*/
ret = sum();
/*
push 14H
push 0Ah
call sum
*/
ret = sum(a, 40);
/*
push 28H //传立即数 直接入栈
mov ecx, dword ptr[ebp-4] //传变量 分两步
push ecx
call sum
*/
}
测试用例
1.使用参数带默认值的函数,成功使用
2.只给右侧参数赋值,左侧使用默认值,语法错误
3.多次声明的成功条件和失败条件
inline内联函数
#include<iostream>
using namespace std;
/*
问题1:inline内联函数和普通函数的区别
1.开销 2.是否在符号表中生成符号 3.是否一定内联(内联一定没符号)
问题2:为什么不说原理只说定义
问题3:inline为什么快 在编译过程中,没有函数调用的开销了,在函数的调用点直接把函数的代码进行展开处理
问题4:inline函数不再生成相应的函数符号
问题5:是不是所有的inline都会被编译成内联函数 不是,递归不会
inline只是建议编译器把这个函数处理成内联函数,最后是否处理成内联函数是由编译器决定的
递归结束的情况只能是在运行的时候等待递归结束的条件 在编译阶段不会知道递归到底调用了多少次 编译器无法展开inline
代码太多会导致展开后命名冲突
debug版本上inline是不起作用的 -g inline 只有release版本才能出现
g++ -c run main.cpp -02 objdump -t main.o 无法找到sum符号
*/
inline int sum(int x, int y)
{
return x + y;
}
int main()
{
int a = sum(12, 30);
/*
此处有标准的函数调用过程 参数压栈 函数栈帧的开辟和回退过程
有函数调用的开销
x+y 的本质是mov add
但是栈帧的调用 标准的函数调用过程使用的指令远远大于三条
*/
//函数的开销 远远大于函数本身执行的时间 在这里就可以使用内联函数
cout << a << endl;
return 0;
}
函数重载
#include<iostream>
using namespace std;
/*
函数重载
1.C++为什么支持函数重载,C语言不支持函数重载
c++可以调用函数名相同的函数 但是C语言不行
C++产生函数符号的时候,函数名+参数列表类型组成的
C产生函数符号的时候,只由函数名决定 C语言将会找到多个符号的定义 报错
一组函数要称得上重载,它的前提是处在同一个作用域当中,在函数调用点寻找最近作用域
注意const和volatile出现后,是怎么影响参数类型得
一组函数,函数名相同,参数列表相同,仅仅只是返回值不同,这种不叫重载,理由返回值和生成的符号无关,无法影响
2.函数重载需要注意些什么
3.C++/C语言之间如何互相调用
【注】啥是多态
静态多态:编译时期,函数重载,在编译时期生成指令就要确定好到底调用那个函数/模板
动态多态:运行时期,
什么是函数重载?
函数名相同,参数列表不同
*/
//函数重载的形式,函数名相同,参数列表不同
//一组函数,其中函数名相同,参数列表的个数或者类型不同,那么这一组函数就称作函数重载
//通过调用的实参选择合适的函数重载版本
bool compare(int a, int b)
{
cout << "compare_int_int" << endl;
return a > b;
}
bool compare(double a, double b)
{
cout << "compare_double_double" << endl;
return a > b;
}
bool compare(const char* a, const char* b)
{
cout << "compare_char*_char*" << endl;
return a > b;
}
// const type 和 const 不构成重载 编译器认为两个函数生成的符号一致
void func(int a) {}
//void func(const int a){}
int g_data = 10; //全局变量
int main()
{
int data = 20;
int a = data; //a 等于20 局部作用域
int b = ::g_data; //b 等于10 全局作用域
//C++中 函数中不能定义函数 但是可以声明函数
bool cpmpare(int a, int b);
compare(10, 20);
compare(10.0, 20.0);
compare("aaa", "bbb");
/*
为什么在声明后不调用重载,反而调用int,int型重载
作用域问题:优先在当前最近的作用域(局部作用域)中找匹配函数
重载必须以作用域为前提 函数内局部作用域由函数声明就不会重载全局作用域的函数
*/
return 0;
}
函数模板
C/C++互相调用
#include<stdio.h>
int sum(int a, int b) //sum .text
{
return a + b;
}
//同样 C调用C++代码
//给C++代码 关键代码加上 extern "C"标记 将C++代码按照C语言方式编译 可以让C文件直接识别出C++代码 因为代码是按照C语言编译的 所以生成的符号C文件可以直接识别
int main()
{
int ret = sum(10, 20); //_sum *UND*
printf("ret:%d\n", ret);
return 0;
}
/*
总结 C 调用C++ 将C++源码括在extern"C"里面
C++ 调用C 将C语言函数声明括在extern"C"里面
*/
/*
拓展:
//C++编译器识别出了宏 所以extern里面的执行 编译成了C符号规则
//C编译器 不能识别出__cplusplus__的宏 所以不执行 直接就是函数体
#ifdef _cplusplus
extern "C"
{
#endif
int sum(int a, int b)
{
return a+b;
}
#ifdef _cplusplus
}
#endif
编译器内置宏 __FILE__文件位置
__LINE__文件行数
__cplusplus
*/
#include<iostream>
using namespace std;
//头文件展开 = 在此加函数声明
//错误 LNK2019 无法解析的外部符号 "int __cdecl sum(int,int)" (? sum@@YAHHH@Z),该符号在函数 _main 中被引用
//int sum(int a, int b); sum_int_int按cpp生成 *UND* 是对符号sum的引用 于是就去其他文件中找 C生成的符号按照C++的规则无法匹配到
//导致C++无法直接调用C代码
//把C函数的声明 括在extern C里面
extern "C"
{
//告诉C++编译器 函数是按照C语言生成符号的,不要按照C++语言来生成符号 按C的符号生成sum 可以在C源文件中找到定义
int sum(int a, int b);
}
int main()
{
int ret = sum(10, 15);
cout << ret << endl;
return 0;
}