因为世界是物质的,物质是运动的,所以cpp基础语法也可以分为物质和运动两个部分
物质部分---------------------
数字
变量
整数
由以下几种不同类型,不同类型决定了占用空间(与操作系统有关)
type | size /byte | abbr |
---|---|---|
char | 1 | c |
short | 2 | hd |
int | 4 | d |
unsigned | 4 | u |
long | 8 | l |
long long | 8 | ll |
bool
有的时候可以借助1bit的bool来实现一些功能(实际上是1byte,为了数据对齐)
bool i;
- 求反
(1-i)
- 正负
(2*i-1)
- 首位(数组)
i*n
- 顺序升降
a>b ^ i
- 存在与否
a*cmp+b*cmp
浮点数
type | size/byte | structure | abbr |
---|---|---|---|
float | 4 | 1 8 23 | f |
double | 8 | 1 11 52 | lf |
规范由IEEE确定
格式
对单精度浮点数而言 structure sign / biased exponent /fraction(碎片)
即 符号位(S) 阶码(E) 尾数(M) v a l u e = ( − 1 ) S ∗ M ∗ 2 E value = (-1)^S*M*2^E value=(−1)S∗M∗2E
尾数
最大取0b11111111,处理成2,则
m a x = 2 128 = 3.4 E 38 m i n = 2 − 126 = 1.18 E − 38 max=2^{128}=3.4E38 \quad min=2^{-126}=1.18E-38 max=2128=3.4E38min=2−126=1.18E−38
大小是按跟0的距离
精度计算 2 23 = 8388608 2^{23}=8388608 223=8388608精度为7位左右
阶码
在储存时会进行移码,即实际储存的是原数据+127
即原范围是[0,255] 两端-127 =[-127,128] 00000000,11111111特殊处理->[-126,127]
规格数(normal number): 因为二进制尾数都是以1开始,所以存储时,只存小数点后面的数
非规格数(subnormal number):阶码全为0,隐藏的整数部分为0,用来储存0
特殊数(non-number):阶码全为1,inf和nan
特殊浮点数
在浮点数运算过程中产生,在我们输入时,可以利用这个漏洞
nan : not a number 无效数
内存 : 阶码全1,尾数非全0的表示无效数NaN
ind : indeterminate 不确定数
产生 : 对负数开方,求对数,0.0/0.0,0.0*inf,inf/inf,inf-inf(0/0会报异常)
算术运算:与任何数进行算术运算结果都是nan
关系运算:结果都为0
inf : infinite 无穷
内存 : 阶码全1,尾数全0表示无穷大INF
产生 : 1.0/0.0=inf , -1.0/0.0=-inf ,log(0)
运算与普通数据相同,只不过有的运算会产生nan
判断
我们可以利用math.h中的下面的函数来判断上面几种数
isfinite(x),isnormal(x),isnan(x),isinf(x)
(与isfinite不同,isinf会返回±1来区分无穷)
模仿提款系统漏洞
先输入nan将mid感染,然后输入5000000,进入牢房
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
//零钱提现,经过mid中转
float change = 100,mid=0,account=100, temp;
printf("change: %.2f mid: %.2f\n",change,mid );
scanf("%f",&temp );
if (temp > change)
{
printf("wrong1");
return 1;
}
change -= temp;
mid += temp;
printf("change: %.2f mid: %.2f\n", change, mid);
scanf("%f", &temp);
if (temp > mid)
{
printf("wrong2");
return 1;
}
mid -= temp;
account += temp;
printf("mid: %.2f account: %.2f\n", mid, account);
}
数组
数组是一种用于存储相同类型的元素序列的数据结构
int numbers[5]{}; // 声明一个包含5个整数的数组,初始化为0
int numbers[] = {1, 2, 3, 4, 5}; // 自动确定数组大小并初始化元素
int size = sizeof(numbers) / sizeof(numbers[0]); // 计算数组的大小
for (const auto x : numbers) cout << x << " ";
int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 二维数组
静态变量
静态与动态相对,静态为编译时能够确定的,动态为运行时能够确定的
在程序被调用时被执行,在程序销毁时销毁,生命周期不限于函数内,相当于写在函数内的全局变量
可以结合类的构造函数和析构函数
一般为全局共享,也可以设置为私有的
常量
不可以修改的量
const double pi =3.1415926;
字符
编码规范,windows中现行两种,基于ASCII的ANSI和全球通用的UNICODE
ascii
char与字符的映射,如下表
特点是简单,易学,最好记住数字和字母的ascii码范围,C语言基于此字符集
ANSI
英文部分使用ascii编码表,中文一般用GBK或GB2312编码
特点
在前期学习的时候方便使用
利用资源看起来比较小
另外,可以自由的配置不同的字符集,同时也是缺点,不支持不同的字符集
在没有该字符集的区域需要额外下载字符集
UINCODE
在实际windows开发时更适合
因为自windows_NT后,windows所有版本都是由unicode来构建的
所以ANSI在执行时会先转换成unicode字符,效率较低
但在基础cpp学习时不方便
char
c语言基于char的字符串
输入
gets(str);//把一行输入存入str
长度
strlen(str);
sizeof(str);//后者比前者多1 即结尾的'\0'
拷贝
char s1[] = "1234", s2[] = "2345",s3[6]="a";
memcpy(s2, s1,2); //s2="1245 memcpy(目标,源头,字节数);//注意是字节数
strcpy(s3, s1);//就是memcpy
strncpy(s3, s1,6);//需要自己追加\0 n为拷贝的个数
s3[6] = '\0';
连接
char s1[10] = "1234", s2[] = "2345";//s1的大小必须足够装下s2, concatenate
strcat(s1, s2);
比较
char s1[10] = "1234", s2[] = "01234";
int a = strcmp(s1, s2);//返回-1 +1 0,相当于比较大小了
stricmp(s1,s2);//比较两个字符串,不区分大小写 i: ignore
strnicmp(char *s1,char * s2,int n);//比较前n位
查找
char *strchr(char *s,char c)//查找单个字符c
int strcspn(char *s1,char *s2)//查找字符串s2
string
cpp中自带的数据类型,通过类实现
输入
//cin 以空格分割
string word;
while(cin>>word){}
//getline 以换行分割
string s;
getline(cin s)
cin.get();//注意读取上次留下的换行符
初始化
string str1,str2,str3;
//直接初始化
str1 ="hello world!";
str2 ="c++";
str3=str1+str2;
//构造函数
string str(3, '6');
str = string(3,'9');
长度
sizeof(str);//其大小是从heap中动态分配的
str.size(); //不看'\0' cpp中类的数据一般都用这个获取长度
字符与数字转换
to_string(1234);
stoi("1234");
//这里的i是指转换成什么格式,有下面几种可以替换
//i:int l:long f:float d:double ul:unsigned long ...etc都是首写
成员函数
string str{"12345",1,3};//str="234"
//如上,成员函数一般都是操作两个字符串,包含下面五个可选参数
//str1.function(str1_pos,str1_num,str2,str2_pos,str2_num);
//_pos表示位置,_num表示参与位数,数字会优先认为是_num,再试_pos;如果不填就是从0开始的全部
str.compare();//也可以用关系运算符 返回值一样
str.substr();
str.replace();
//下面
str.rfind(snew,_pos);//返回首字符地址 没找到返回 std::string::npos//no possible
str.insert();//第一个参数位放插入位置
str.erase();
str.clear();
const char* cstr=c_str(str);//转化为c风格字符串
wchar_t
unicode用此表示,字符要用L修饰,还需要像如下进行设置才能正常使用
作为了解即可,算法学习用不上,windows编程中有自带的CString类型支持unicode
#include<iostream>
int main()
{
wchar_t s[] = L"中国";
wchar_t s1[3];
wcscpy(s1, s);//另外还支持wcslen,wcscat,wcscmp,wcsstr(查找)
setlocale(LC_ALL, "chs");
wprintf(L"%s", s1);
}
指针
指针的不同类型决定的是操作的位数
指针具有类型与指向地址的两个属性
指针只是一种特殊的数据,其大小跟操作系统有关
变量 通过操作系统来控制空间,直接控制值
指针 直接控制内存空间,间接控制值
对结构体返回指针就避免了赋值,节约内存
指针虽然需要类型相对应,但我们可以利用一个中间指针来强制改写地址
int *mid = (int*)&BeAtk;
mid[0] = 0x41fd40;
函数指针
//对于函数最重要的是*参数和返回值的类型*
int add(int a, int b)
{
return (a + b);
}
//指针定义,初始化
int (*padd)(int a,int b) {add};//也可写成(int,int)
padd(1,2);//与函数使用方法相同
//对结构体定义指针时,应依次传参
//强制转换
short(*pfadd)(short,short) =(short (*)(short,short))add;
//简化写法 #define typedef一样
using pfadd=short(*)(int,int);
typedef short(*pfadd)(int,int);
Pfadd padd=(Pfadd)add;
//将函数指针作为参数使用
int test(int a,int b,pfadd x)
{
return x(a,b);
}
指针函数就是下面返回值为指针的那个
数组与指针
int a[]{ 1,2,3,4,5 };
int b[2][5]{ 1,2,3,4,5,6,7,8,9,0 };
int* pa[5]{a};
//指针数组:指针为元素的数组
//数组的本质是指针,指向第一个元素,所以也可赋值成{&a[0]}
int* pb = (int*)b;
//多维数组:取址时 需进行一次类型转化,因为多维数组需要多传递一个信息(行列数),不是纯粹的地址,而是数组指针
int(*ppb)[5] {b};
//数组指针:指向数组的指针 用()来表明是指针 默认处理为数组
//没有引用数组,所以定义'数组引用' 必须打括号
智能指针
//唯一智能指针
//不能其他指向,避免了野指针
std::unique_ptr<int[]> smartptr{ new int[5] {1,2,3,4,5} };
//不能std::unique_ptr<int[]> smartptrcopy{smartptr};
std::unique_ptr<int[]> smartptrA{ std::make_unique<int[]>(5) };//c++14把后面[]去掉后表示初始化为5
smartptr.reset();//释放空间,地址清零
int *a = smartptr.get();//返回原始指针
a = smartptr.release();//不释放空间,地址清理,返回原始指针
smartptrA = std::move(smartptr);//智能指针之间的数据转化
//共享智能指针
//不能以数组形式访问,赋值与unique大致相同,当最后一个被释放时内存才释放,用reset只会清除自身
std::shared_ptr<int> smartptrshare{ new int[5] {1,2,3,4,5} };
std::shared_ptr<int> smartptrshareA{ smartptrshare };
cout<<smartptrshare.use_count();//返回指针被指向了多少次
smartptrshare.unique();当最后一个调用时返回true c++17已废弃
指针安全
//指针安全
//野指针,悬挂指针, 指针与内存空间释放不同步//大部分程序员是没有对象的野指针
int* p;
{
int* a = new int[10];
p = a;
}
cout << p[2];//a 的生命周期已结束{}内,而内存还在,用智能指针可以解决问题
内存管理
分配内存后一定要释放,不然会导致内存泄漏,即内存被不需要的垃圾充满,从而导致程序崩溃
unsigned size;
cin >> size;
//内存管理 c
int* p1 = (int*)malloc(size * sizeof(int));
//memory allocate 内存分配 dynamic 返回值为void,没有类型,只有一个地址,赋值前应先转化
int* p2 = (int*)calloc(size, sizeof(int));
//c=count ,分配后会将内存清零,但内存占用更大
//分配之后p就可以当作一个有size个元素的数组使用
//内存分配失败时return 0 0也可以写成nullptr(in Cpp)
p2 = (int*)realloc(p2, size);//re-再次
//先搜索附近有没有空位置,没有再往后移,并拷贝之前的数据
free(p1); p1 = 0;
//释放内存,并将指针初始化,下次使用时不会出错
//内存管理 cpp
p1 = new int[size];//用malloc实现,可以用于动态大小的数组
p2 = new int;
delete [] p1;//不用参数
delete p2;
//备份数组
int num[5]{ 1,2,3,4,5 };
int* pnum = new int[5];
//随机分配,地址不确定,而直接int num[100]{}的地址可通过对栈的加减进行确定
memcpy(pnum, num, 5*sizeof(int));//(目的地,原数据,大小(单位为字节))
memset(pnum, 0, 20);//将所有pnum中的数据变成零{char类型都可以
指针类型
//输出与运算都与类型有关,小内存指针操作低位,高位不变
//p++是按4/8为单位自增
unsigned ua{ 1 };//输出类型
int* pua=(int*)&ua ;
*pua = -1;//0xFFFFFFFF
char* pc1 = (char*)pua;
*pc1 = 'a';//0xFFFFFF41
cout << ua;
多级指针
int a[]{ 1,2,3,4,5 };
int* pa = &a[0];
int** ppa = &pa;
*ppa = &a[3];
//定义动态二维数组
int n;
cin >> n;
int** num = new int* [n];
for (int i = 0; i < n; i++)
{
num[i] = new int[2];
cin >> num[i][0] >> num[i][1];
}
常量与指针
//常量指针
const int con1 = 514;
const int* pcon1 = &con1;//可以修改指向对象,不能改变对象数据
//指针常量
int var = 114;
int* const pcon2 =&var;//不可修改指向对象,可以修改对象数据
//指向常量的指针常量
const int* const pcon3 = &con1;//啥都不准改,风雨不动安如山
//字符串是常量,其两种定义方法
char str[] = "hello world";
char* pstr1 = (char*)str;
const char* pstr2 = str;
空指针
NULL和nullptr,可能会导致类型推断错误,即调用到并非我们期待的函数
NULL宏定义(void*)0,很多时候被直接处理成0,
nullptr具有类型nullptr_t,会按指针处理
#include <iostream>
void foo(int x) {
std::cout << "foo(int) called" << std::endl;
}
void foo(char* ptr) {
std::cout << "foo(char*) called" << std::endl;
}
int main() {
foo(NULL); // 会导致类型推断问题,如果是想以指针调用
return 0;
}
引用
引用是另类指针
引用,给一个变量取不同的名字,后续不可改变引用对象
但必须用实体初始化,指针可以不初始化
引用是一个更接近实体的东西
int a{ 1000 };
int& la{ a };
int *"e=ptr;//对指针的引用,可以更改指向对象
//对外表明自己是实体,背地里按指针的方式处理问题
右值引用
左值有明确的内存空间,在栈里,能写入,如定义一个变量并赋值
右值临时分配内存空间,在堆里,不能写入,如 1+1
一般左值在等号左边,右值在等号右边 int a[10]; *(a+1)=100;
int && e{100+231};
//与二级指针不同,&&仅为引用右值,不能用来存放引用
组合
自定义
#define version "v1.0"
typedef long long int64;//注意先后顺序
using pfadd=float(*)(int,int);
结构体 联合体
结构体在使用时与int之类的一样,但传递时会申请内存空间再赋值
应该使用指针,只传递一个地址
一堆数据的捆绑
结构体
//内存对齐,sizeof(Role);返回12,依次堆叠,就像俄罗斯方块一样,小的可以进入上面的空位,而大的为封锁空位
typedef struct Role//自定义type
{
int HP;
int MP;
short LV;
}*Prole,*Nbrole;
//自定义结构体,并可同时定义多个指针 Prole与Role*等价
//定义结构体是给编译器看的,不会翻译成汇编代码,而是作为后面翻译的字典
//结构体来表示个体的属性,数组来表示个体
//结构体里面放函数,面向对象,但最好别这么写,面向对象用class定义类
//c++底层也是利用的这个
struct role
{
int hp;
int atk;
int act(role* beacter)
{
beacter->hp -= atk;
return beacter->hp;
}
};
int main()
{
role a{ 1000,100 }, b{ 1200,80 };
a.act(&b);
}
C中的结构体
typedef struct node
{
ElemType data;
struct node*next; //此处的类型定义还未完成,仍需用struct node来调用
}node;
联合体
//共享内存,其大小与最大数据类型大小相同,后面赋值数据直接覆盖前面
union once
{
short lv;
};
union
{
int b;
}only;//只用一次的结构体,变量名only来存放,可以嵌套用在结构体中
命名空间
嵌套命名空间
namespace game//数据,函数仓库
{
int hp(1000);
int mp(2000);
int lv(0);
namespace weapon
{
int atk(50);
int ap(100);
}
}
using namespace std;//将会将所有的引用进来,浪费内存,定义冲突
全局命名空间
使用全局命名空间::p是为了防止与局部同名标识符发生冲突
int p=100;
int main()
{
int p=10;
std::cout<<::p;
}
命名空间的扩展
namespace std
{
float pi=3.14159;
}
int main()
{
std::cout << std::pi;
}
声明与定义
//声明在test.h
namespace htd
{
extern float pi;
void test();
}
//定义在test.cpp中
# include "test.h"//也可以将声明复制一遍
float htd::pi = 3.14159;
void htd::test() { std::cout << "hello!"; }
未命名的命名空间
仅对本转换单元有效
namespace
{
void hack()
{
std::cout << "Thanks\n";
}
}
int main()
{
hack();
}
命名空间的昵称
可用于对于多级命名空间简化
htd::hack::safe();//原本的样子
namespace a = htd::hack;//取昵称
a::safe();//调用的样子
枚举
enum class weapon :int
//默认为int,但使用时必须先先转换
//enumerate枚举提高安全性,可读性
{
normal = 1, armA = normal,//值为递增关系,插入时自动更改
high, armB = high,
epic, armC = epic,
legend, armD = legend,
myth, armE = myth
};
weapon sword{ weapon::normal };
//赋值方式
short dif = (int)sword - (int)arch;
运动部分---------------------
运算
优先级
单目>算术>位移>判断(不等>等)>位运算>逻辑>条件>赋值…
具体如下
单目运算
-
读取数据和函数
:: . -> ()
sizeof();//获取占用内存string 32 vector 24
算术运算
隐式类型转换
int<long<longlong<float<double<longdouble tip:short,char都转化为int
unsigned>signed
时间复杂度
+ -
1tick *
3tick /
30tick
-其实就是补码加法,在循环条件中,能用乘法就不要用除法
关系运算
3.14 == pi ;
以此种规范写,可以防止一时疏忽从而没区分 = 与 ==
位运算
& | ~ 与或非 << >>左右移
#include<bitset>
int a = 1000;
cout << (a >>= 4)<<endl;
//左右移 乘除2^n unsigned 都补1 int补1/0是看原数字正负,但有些计算机不支持
cout << ~a<<(char)10;
//求反 将所有二进制反,a+(~a)=0b11...1=-1
cout << (0xffff0ff0 & 0xff00fff0)<<endl;
// 与 游戏颜色4bytes 透明+rgb 可用于读某个位置的数,读后可以进行位移
cout <<std::bitset<8>(0b10010011 | 0b00100110) << endl;
//或:与搭配,进行加减
int d = 123, b = 241, c = b ^ d;
//用c来记录a,b的关系,然后再还原 ,异或运算的三角关系
cout << (c ^ d) << endl << (c ^ b);
//可用于检验其中一个数据被篡改
//单机游戏检测到作弊后,降低爆率,卡bug来制裁
//对于对立事件,如按大小顺序进行排序
leetcode 1684 利用位运算来记录字母是否出现
其他
逻辑运算
&& 将短的条件放在前面
条件运算
三个数比大小
int max = a > b ? (a > c ? a : c) : (b > c ? b : c);
赋值运算
对于二维数组赋值运算,应先赋值连续段
控制语句
判断
多个条件并列时,简单的放在前面,提高效率
if (){}
else if (){}
else{}
//if else就近匹配
//简单判断用 ? :
switch (grade)
{
case 1 :cout<<("A"); break;
case 'A':cout << ("B"); break;//ascii 65
default: cout<<("expired grade");
}
循环
路由器pin码,crackpass爆破
int pin{ 12345678 };
int crackpass{};
re:
if (crackpass != pin) {crackpass++;goto re;}
for (int crackpass = 0; crackpass != pin; crackpass++);
string s1 = "hello world";
for (auto x : s1)cout << x << endl;
while (crackpass != pin)crackpass++;
crackpass = -1;
do{crackpass++;} while (crackpass != pin);//效率更高,while会被优化成这个
cout << crackpass;
//循环数组 c++11
int arr[5]{ 1,2,3,4,5 };
for (int i : arr) //此时有个强制转换
cout << i << endl;
递归函数
int fac(int x)
{
if (x == 1)return x;
return x * fac(x - 1);
}
//循环的效率更高,但递归逻辑更清晰?我不觉得
//如果不设置退出条件,栈会被堆满崩溃
函数本质上是功能模块化,使代码更清晰,便于维护和使用
学习函数 : 输入输出/作用
函数
三个部分
参数 : 类型 , 默认选项or自己定义
调用约定 : 描述了函数参数是怎么传递和由谁清除堆栈的。
返回值 : 类型 , 注意错误时的返回信息
在编译时会在函数名前加一堆东西来区分
另外还有函数名和函数体
参数
- 参数
//形参 即用来占位的形式参数 数组参数 参考冒泡排序
void cprint(char str[]){}
//默认实参 可以不输入,按照默认值进行,只能放在末尾
//指针参数 用于操作外部数据,利用指针和引用传参可以减少内存开销
//常量指针参数 只读取,不改变 ...直接用形参也一样
- 不定参数
//主函数
//不定参数,用于cmd调用程序 最后有一个空指针
//用空格打断 依次放入数组,注意my.exe也算参数 当文件名有空格时,可以用" "来整体化
int main(unsigned count, char* arc[]){}
//自定义函数
//鸡肋在于需要自己输入参数个数
#include<stdarg.h>
int average(int count,...)
{
va_list arg; //char指针 也可以写成char*arg;
va_start(arg, count);//将参数地址放入arg
va_arg(arg, int);//以int规则读取参数,读取后指针偏移sizeof(int)
va_end(arg);//释放指针
}
- 不定参数不使用count
template<typename T,short count>
T sum(T(&arr)[count])
{
T sum{};
for (int i = 0; i < count; i++) sum += arr[i];
return sum;
}
int main()
{
int a[4]{ 1,2,3,4 };
cout << sum(a);
}
调用约定
描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下三个方面:
- 函数参数传递的方式(是否采用寄存器传递参数、采用哪个寄存器传递参数、参数压桟的顺序等)
- 函数调用结束后的栈指针由谁恢复(被调用的函数恢复还是调用者恢复)
- 函数修饰名的产生方法
实例化用下面两个函数
int add ( int a, int b );
调用约定 | 参数传递 | 恢复栈指针 | 函数修饰名 | 注意 |
---|---|---|---|---|
__cdecl(C declaration) | 栈,从右向左入栈 | 调用者 | _func _add | 调用参数可变的函数只能采用这种方式(printf) |
__stdcall(__pascal) standard | 栈,从右向左入栈 | 被调用者,常用于WINAPI中常用 | _func@num(byte) _add@8 | 因为被调用者清空,就只需要被调用者编译出清空的相关代码.节省空间 |
__fastcall | 寄存器和栈,从左往右的前两个由ecx和edx传递,其余从右向左由栈传递 | 被调用者 | @func@num@add@8 | 有寄存器传递,速度会快很多 |
__thiscall | 栈(寄存器),从右向左入栈,参数个数不确定时,this指针最后入栈,反之,通过ecx传递,之后一般存入esi中 | 参数个数确定时,调用者清理,反之 | 唯一一个不能明确指明的函数修饰 | 类的成员函数使用的调用约定 |
返回
- 数组
//指针是局部变量,函数结束后消失,而new空间会放在heap里,直到释放
int clen(const char* str)
{
int i = 0;
for (i; str[i]; i++);
return ++i;
}
char* cstr(const char* str)
{
int len = clen(str);
char *str1= new char[len];
memcpy(str1, str, len);
return str1;
}
- 指针和引用
返回结构体会给每个数进行赋值,内存开销大,可直接返回指针
//指针
return &a;//返回的地址应该是new的,否者在执行完函数后,内存会被清空
//引用
return *a;//返回的表面上应该是实体
- return与exit
return是语言级别的,它表示了调用堆栈的返回,会隐式调用exit
exit是系统调用级别的,它表示了一个进程的结束
exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件
函数的工具
函数重载优先级大于函数模板
函数重载
功能类似时, 对不同函数用同一函数名, 不过只有cpp中能使用
通过参数来区分,先参数个数,再参数类型,类型不同再强制转换
引用会出现歧义 add(int &a,int &b); add(int a,int b);二者都是传送实体
强制转换后匹配引用失败,因为转换的只是括号里的临时变量,没有自己固定的内存地址
const int 与 int 也不能区分, 因为调用函数本来就不会改变参数的值
函数模板
- 解决对同一逻辑的,固定参数数量,不固定参数类型的函数使用
用来生成函数的模板,本质就是无脑替换
传送引用时,传送的实际是内存地址,不会实例化,因为引用和指针在返回时并不是以原形式返回的,需要类型转化,而模板会全部当同一类型来看
函数模板是对函数的补充,所以模板之下,完全按函数的套路来
使用一种函数就会生成一个函数,内存占用up
只在编译器层面存在
//模板声明
template<typename T1,typename T2>
T1 sum(T1 a, T2 b)
{
return (a + b) / 2;
}
sum<int,float>(100,2.5);//转换时的两个人生导师
//当返回值类型不能由前方运算得到时,必需要进行指定,指定是依次来的,所以最好把返回类型放在第一位
//一些特殊方式(其实也就是把函数的初始化推广一下
//默认类型
template<typename T1=double>
//非类型的模板参数,写在前面好赋值
template<int x,int y =100,typename T>
T mul(T z)
{
return x*y*z;
}
mul<10>(5);//调用时,未指定的需要指定
//模板与具体混用
T1 sum(T1 a, int b)
- 解决指针问题
用于处理指针和引用
template<typename type>
type bigger(type a, type b)
{
return a > b ? a : b;
}
//用例外解决
//例外优先级比模板高
template<>
int* bigger(int* a, int* b)
{
return *a > *b ? a : b;
}
//用模板解决
int* bigger(int* a, int* b)
{
return *a > *b ? a : b;
}
- 解决参数数量不确定, 用 函数模板的重载
template<typename type>
type aver(type a, type b, type c)
{
return (a + b + c) / 3;
}
template<typename type>
type aver(type a, type b)
{
return (a + b) / 2;
}
>decltype
c++11后可用
注 : auto会自动删除const和引用属性,直接显示值
- auto
//拖尾函数,来解决auto自动删除问题,一般不建议,太麻烦了
auto bigger(int &a, int& b)->int&
{
return a > b ? a : b;
}
//用int& 代替auto也一样
//不用拖尾将无法赋值
int a=10,b=20;
bigger (a, b)=10;
//在c++14后才能用auto来判断返回值的类型
- decltype
declare type 解决auto自动删除问题
//I.未经历运算时
//1.就是a的类型,不删除const和引用属性
decltype (a) x;
//2.如果是函数,直接是返回值类型,不会执行表达式
decltype(bigger(a,b));
//II.经历运算后
//1.如果有固定内存地址,则得出该类型的引用
int*pa{&a};
decltype(*pa) x=a;
//2.反之,得出结果类型
decltype(a+0.1) x;//double
//可将->int&改成->decltype(a>b?a:b)
//c++14后可直接写成 ,返回的是引用类型,可对其进行修改
decltype(auto) bigger(int&a,int&b){}
- 应返回结果与实际返回类型不同时
template<typename T1,typename T2>
decltype(auto) bigger(T1 a, T2 b)
{
return a > b ? a : b;
}
int a = 100000;
float b = 2;
bigger(a, b);
//此时经过计算后,因为类型转化,应当返回float类型的引用,但实际a为int类型,无法被float引用
//此时此刻,返回的为float类型,相当于做了一次类型转换
- 编译器角度理解定义和声明
x32下每个进程都有4GB的内存
全局变量在全局区,有着相对固定的地址,局部变量没有
游戏中,会变得放在堆区,不会变的放在全局变量区
函数的本质
函数名的本质就指令开始的指针
int add(int a, int b)
{
return (a + b);
}
//函数名是内存地址,下面代码的两个输出是一样的
cout<<add<<" "<<&add
//函数体是编译好的二进制代码
char*str=(char*)add;
for (int i = 0; i < 30; i++)
printf("%02X \n", (unsigned char)str[i]);
函数的具体实现清跳转other->assembly
Lambda 表达式
匿名函数
[ ]( )->int{ }
捕获列表 参数类型 返回值类型 函数体
捕获列表,获取作用域内的变量
返回值类型可不写,编译器可以自动判断
参数类型和函数体跟不同函数一样
- 捕获列表
[&]按引用捕获 [=]按值捕获 [K=5] 引入新的变量
不加变量就会默认捕获所有全局变量
int a = 10,b=0;
auto f = [b, &a](){a+=b;};
f();
- 用在sort函数中
C/C++联合编程
static与inline
- static
静态变量
int test()
{
static int count{};
cout << count++ << endl;
}//生命周期与全局变量一样,但只有在该函数中才能访问
静态函数
static int test(){}
//具有内部链接属性
- inline
内联变量
//c++17才能用,具有外部链接属性,可以多次定义,但最好不要这么做,放在头文件中比较好
inline float pi =3.14159;
内联函数
该函数会建议编译器处理成内联代码以提升性能,短小的函数可以内联
但现在编译器比较智能, 会自动优化,inline的可能不内联,没inline的可能内联
建议放在头文件里,编译更快(内联的本质就是去除调用的过程,在汇编层面直接实现相应功能)
inline int sum(int a, int b)
{
return a + b;
}
声明与定义
意义 : 函数过多时,代码更清晰
声明是各种类型的指针
定义是指针指向的内存地址
//声明
int sum(int a,int b);//也可写成int sum(int ,int)跟函数指针同理
int sum(int a1,int b2);
extern int sum(int ,int);
//定义
int sum(int a, int b)
{
return a + b;
}
//定义只能有一次,而声明可以有无数次
//声明不需要内存分配,定义需要
- 对于变量 的声明与定义
extern int all;
int all {1000};
自己的头文件
头文件查找顺序
- 系统头文件:包括C++标准库的头文件(例如iostream、vector、string等)和操作系统提供的头文件(例如Windows.h、ImageHlp.h等)。
- 第三方库的头文件:如果你在代码中使用了第三方库,那么应该先包含该库的头文件。
- 自定义头文件:包括你自己编写的头文件和项目内部的其他头文件。
编译时,先将.c/.cpp文件编译为.obj文件,再将.obj文件链接为.exe可执行文件。头文件如果不被调用,就不会被编译。
为了避免编译错误,应该遵循声明n次、定义一次的原则。编译时,先将.c/.cpp编译为.obj文件,再链接为.exe,头文件如果不调用就不会编译
头文件.h用来声明需要使用的函数,也可以写定义,但是有时需要嵌套调用,会导致同一个函数被多次定义。不过,可以使用静态函数和内联函数来定义,因为静态函数只在它所在的源文件中有效。
引用头文件的方式是使用#include"emath.h"。
头文件的开头可以使用#pragma once来防止多次定义。也可以使用#ifndef _HEMATH_和#define _HEMATH_的组合来达到相同的效果,但只对单个源文件有效。这样做的目的是为了防止头文件被多次引用,提高编译速度,因为头文件只会被打开一次。
当其他工程需要使用自己的头文件时,可以先拷贝该头文件,然后将其添加到新的项目中。
以上是关于自己的头文件的一些说明和建议。通过正确的头文件使用和包含顺序,可以避免编译错误和依赖问题,提高代码的可读性和可维护性
cpp调用c语言定义的函数
- 头文件中声明时
extern "C" int sum();
- 也可批量声明
extern "C"
{
int sum();
int aver();
}
- 也可在源文件中引用
extern "C"
{
#include "emath.h"
}
c语言无法调用cpp的函数,除非在声明时,用这种格式
extern "C" int sum()
- 当.h中的文件以extern "C"声明后,c语言调用会报错,怎么办呢,用这个方法
#ifdef __cplusplus
extern "C"
#endif
int sum();
//c语言无法识别这个宏,而cpp能
声明为c风格函数后,函数就无法重载了