2022/9/15 初始版
2022/10/24 新增24、25两点,第7、21点有补充
2022/12/6 新增26、27两点,新增遇到的错误总结6点
一、C++的一些细碎知识点
1. #和##
1.1 # 把参数转换为字符串
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
#define T(A) #A
#define TEST(X) printf(#X);
#define TEST1(X) printf("This is \#X \n");
#define TEST2(X) printf("This is '#X' \n");
#define TEST3(X) printf("This is "#X" \n");
int main(){
int a = 10;
double b = 10.5;
float c = 15.5f;
string str = "Hello";
cout << T(a) << endl; //a
cout << T(2.2) << endl; //2.2
cout << T(2.5f) << endl; //2.5f
cout << T("abcd") << endl; //abcd
TEST(10); //10
cout << endl;
TEST1(15); //This is #X
TEST2(20); //This is '#X'
TEST3(25); //This is 25
return 0;
}
1.2 ## 把两个宏参数贴合在一起
- ##相当于一个粘合剂,将前后的部分粘合起来,又字符化的意思,但必须粘合C语言合法的标识符
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
#define TEST(Name) c##Name
#define TEST1(a, b) int(a##b##a)
#define P(format, ...) printf(format, ##__VA_ARGS__);
//在__VA_ARGS__宏前面加上##:当可变参数个数为0时,##会把多余的“,”去掉,否则编译会出错
#define P2(format, ...) printf(format, __VA_ARGS__);
int main(){
string cTest = "Test ##";
cout << "cTest = " << TEST(Test) << endl; //Test ##
cout << TEST1(10, 24) << endl; //102410 没有int()也是一样的结果
P("Hello ##__VA_ARGS__\n");
//P2("Hello __VA_ARGS__\n"); //Error
return 0;
}
1.3 注意事项
-
当宏参数是另一个宏的时候,只要宏定义里有"#" 或者是 “##” 的地方,宏参数不会展开,即只有当前宏会生效,参数里的宏不生效(包括系统和库函数中的宏定义)
-
#include<iostream> #include<algorithm> #include<string> using namespace std; #define T (5) #define TEST(a) #a #define TEST1(a, b) int(a##e##b) int main(){ cout << "TEST(T) = " << TEST(T) << endl; //套自定义的宏定义 TEST(T) = T cout << "TEST(INT_MAX) = " << TEST(INT_MAX) << endl; //套库函数中的宏定义 TEST(INT_MAX) = INT_MAX //cout << "TEST1(T, T)" << int(TEST1(T, T)) << endl; //error cout << "TEST1(5, 5) = " << TEST1(5, 5) << endl; //TEST1(5, 5) = 500000 return 0; }
-
解决方案:加一层中间转换宏
-
2. 双冒号的用法(:😃
2.1 域操作符
-
在类A里面声明了一个成员函数void test(),但是没有在类内进行定义(类内定义默认为内联函数)。在类外定义时需要使用双冒号进行定义:
-
class A { public: A(); ~A(); void test(); }; A::A() {} A::~A() {} void A::test() { std::cout << "Test ::" << std::endl; }
2.2 直接用在全局函数前,表示全局函数
-
在VC++里面,可以调用Windows API里的函数,在API函数前面加双冒号即可
-
// 获取进程ID DWORD dwProcessId = ::GetCurrentProcessId();
2.3 作用域成员运算符
-
表示引用成员函数及变量
-
std::cout << "Test ::" << std::endl;
3. 一些VC和Linux环境下的区别
3.1 snprintf
-
gcc中,函数名为snprintf。VC中成为_snprintf,安全函数为_snprintf_s
-
//VC #include <iostream> int main(int argc, char *argv[]) { char buf[50]; std::string str = "1234567890abcd"; printf("%d\n", _snprintf_s(buf, 10, str.c_str())); // -1 printf("%s\n", buf); //1234567890 return 0; } //_snprintf_s的第二个参数表示:会向buf中写入10个字符,并且不会再字符串末尾添加'\0',如果字符串长度超过10,返回-1标志可能导致的错误
-
//Linux 同C++11 #include <iostream> int main(){ char buf[50]; std::string str = "1234567890abcd"; printf("%d\n", snprintf(buf, 10, str.c_str())); //14 printf("%s\n", buf); //123456789 return 0; } //snprintf的第二个参数表示:向buf中写入10个字符,包括'\0',且返回实际的字符串长度。str.size() = 14
3.2 64位整数
-
//Windows //VC6.0 不支持long long的定义以及cout不支持64位长整型 __int64 a; //Win32 64位长整型 printf("%l64d\n", a); printf("%l64u\n", a);
-
//Linux long long a; printf("%lld\n", a); printf("%llu\n", a);
4.占位符
4.1 C格式化占位符详解
占位符 | 说明 | 占位符 | 说明 |
---|---|---|---|
%a,%A | 浮点数、十六进制数和p-记数法(C99) | %c | 字符char |
%C | 一个ISO宽字符 | %d | 有符号十进制整数int |
%ld,%Ld | 长整型数据long | %hd | 短整型数据 |
%e,%E | 浮点数、e/E-记数法 | %f | 单精度浮点数float,十进制 |
%nf | n表示精确到小数位后n位 | %g,%G | 根据数值不同自动选择%f或%e |
%i | 有符号十进制数(同%d) | %o | 无符号八进制整数 |
%p | 十六进制形式输出指针 | %s | 对应字符串char *(%s==%hs==%hS输出窄字符串) |
%S | 对应宽字符串wchar_t *(%ws==%S输出宽字符串) | %u | 无符号十进制整数 |
%x | 无符号十六进制整数(2f) | %#x | 无符号十六进制整数(0x2f) |
%X | 无符号十六进制整数(2F) | %#X | 无符号十六进制整数(0X2F) |
%% | 打印一个百分号 | %lld | 用于INT64或long long |
%llu | 用于UINT64或者unsigned long long | %llx | 用于64位16进制数据 |
%n | 什么也不输出。将在%n之前输出的字符数存储到指针所指的位置。printf(“1234%n”, &num); //不输出 printf(“%d”, num);//num = 4 | %m | 打印errno值对应的出错内容 |
-
%:格式说明的基本符号,不可缺
-
-:左对齐输出,默认右对齐
-
0:指定空位补0
-
m.n:m是域宽(对应输出项在输出设备上所占字符数),n是精度(默认为6)
-
#include <stdio.h> int main(){ int a = 0x355; int b = 0x5; //a全部输出355 超过两位,实际输出 printf("%2x\n", a); printf("%02x\n", a); printf("%-2x\n", a); printf("%.2x\n", a); printf("%2x%2x\n", b, a); // 5355 数据不足两位,前补空格 printf("%02x%02x\n", b, a); //05355 数据不足两位,前补0 printf("%-2x%-2x\n", b, a); //5 355 数据不足两位,候补空格 printf("%.2x%.2x\n", b, a); //05355 效果同%02x return 0; }
5.NULL和nullptr
在C中:
-
没有nullptr
-
NULL是一个宏,被定义为空指针
-
#define NULL ((void *)0)
在C++中:
-
在进行函数重载时,NULL容易出现二义性。C语言可以隐式转换,比如函数重载了int和char两个类型的,传入了NULL,就会出现二义性,编译就会报错。因为C本身不支持函数重载,所以纯C不会出现这个问题
-
//NULL的定义 #undef NULL #if defined(__cplusplus) //C++预处理器宏 #define NULL 0 #else #define NULL ((void *)0) #endif
-
C++中不能将(void *)类型的指针隐式转换成其他指针类型
-
nullptr不是整型类,也不是指针类型,但是能转换成任意指针类型。实际上是std::nullptr_t
#if define(__cplusplus) &&__cplusplus >= 201103L #ifndef _GXX_NULLPTR_T #define _GXX_NULLPTR_T typedef decltype(nullptr) nullptr_t; #endif #endif // C++11
总结:在C++中尽量使用nullptr,在C中使用NULL
6.windows.h和winsock.h冲突
把WinSock2.h放到Windows.h前面
7.new、delete、operator new、operator delete、placement new
7.1 new
-
new分配内存并且返回指向该内存块的指针
-
在该指针指向处调用对应的构造函数构造对象
-
内置数据类型或者组合数据类型的对象的值如不加()是默认不进行初始化的。但是类类型对象将用默认构造函数进行初始化(如string或自定义类)
-
int *p1 = new int; //未初始化 int *p2 = new int[]; //未初始化 int *p3 = new int[10](); //带括号意味着调用了构造函数,进行了初始化
-
注意在指针数组、vector<自定义对象类型指针>等,插入一个 new了的对象时,要每次遍历完都delete掉不用的对象,或者最后编写一个循环的释放内存的函数去释放对象
8.函数调用约定
- VC默认shi用 __cdecldi调用方式
- Windows API使用 __stdcall调用方式
- 由于在DLL导出函数中,应和Windows API保持一致,所以应使用__stdcall调用方式
- 调用约定与堆栈清楚密切相关。如果写一个汇编函数,在 __cdecl方式下,汇编函数无需清除堆栈;而__stdcall方式下,汇编函数则需要在返回(RET)之前回复堆栈
- C:__cdecl、__stdcall、__fastcall、__naked、__pascal
- C++: 比C多了一种__thiscall
8.1 __cdecl
- 又称为C调用约定,是C/C++语言缺省的调用约定。参数按照从右到左的方式入栈,函数本身不清理栈,清理栈的工作由调用者完成,返回值在EAX中。因为由调用者清理栈,所以允许可变参数函数存在。【int sprintf(char *buf, const char *format, …);】
8.2 __stdcall
- 很多时候被称为pascal调用约定。参数按照从右至左的方式入栈,函数自动清理堆栈,返回值在EAX中
8.3 __fastcall
- 特点是速度快,因为它通过CPU寄存器来传递参数。它使用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中
8.4 naked
-
是一个很少见的调用约定,一般不建议使用
-
编译器不会给它增加初始化和清理代码,而且不能使用return返回返回值,只能插入汇编返回结果
-
此调用约定必须跟__declspec同时使用
-
如:定义一个求和程序
__declspec(naked) int add(inta, int b);
8.5 __pascal
- pascal语言的调用约定,和__stdcall一样,参数按照从右到左的方式入栈,函数自身清理堆栈,返回值在EAX中。
- VC已弃用,建议使用__stdcall代替
8.6 __thiscall
- C++特有的调用方式,用于类成员函数的调用约定
- 如果参数确定,this指针存放于ECX寄存器,函数自身清理堆栈
- 如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈
- __thiscall不是关键字,程序员不能使用
- 参数按从右到左方式入栈
9.UID、PID、TID、PPID
9.1 uid
- 用户身份证明(User Identification)的缩写。UID用户在注册后,系统会自动分配一个uid的数值。
- 就是一个用户编号,root用户编号为0,是最高权限
- 它可以通过pid找到这个进程的uid
- 每个不同的应用程序都会有一个uid,是不变的,除非卸载应用
9.2 pid
- 进程识别号,进程标识符,进程控制符(Process Identifier)
- 操作系统里每打开一个程序都会创建一个进程ID,就是pid
- pid是各个进程的代号,每个进程都有唯一的pid(暂时唯一),是进程运行时系统分配的,并不代表专门的进程
- 运行时pid不会改变标识符,但是终止后pid标识符就会被系统回收,可能会被继续分配给新运行的进程
9.3 tid
- 线程控制符,线程id(THREAD Identifier)
- 进程内唯一(暂时),不同进程不唯一
- 一个线程维护一个连接,不过线程不等于连接,一个线程还可以做其他事情
9.4 ppid
- 代表当前进程的父进程ID
10.tchar类型转char类型
void TcharToChar(const TCHAR *tchar_, char *&char_) {
/*int len = 0;
len = WideCharToMultiByte(CP_ACP, 0, tchar_, -1, NULL, 0, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, tchar_, -1, char_, len, NULL, NULL);*/
if (tchar_ != nullptr) {
int tchlen = wcslen(tchar_);
int chlen = WideCharToMultiByte(CP_ACP, 0, tchar_, tchlen, NULL, 0, 0, 0) + 2;
char_ = new char[chlen];
if (char_) {
memset(char_, 0, chlen);
WideCharToMultiByte(CP_ACP, 0, tchar_, tchlen, char_, chlen, 0, 0);
}
}
}
11.根据完整路径提取文件名
void getFileName(TCHAR pszFullPath[MAX_PATH]) {
char *pch = nullptr;
char *temp = { 0 };
TcharToChar(pszFullPath, temp);
pch = strrchr(temp, '\\');
cout << "FileName:" << pch + 1 << endl;
}
12.memset
- 引入头文件<cstring>或者<string.h>而不是<string>,否则会编译报错,显示有二义性,具体未深究
13.数组名
参考《C和指针》第八章 数组
-
数组名的值是数组第一个元素的指针常量
-
数组名不是指针,但大部分情况下编译器会把数组名隐式转换成一个指向数组首地址的指针
-
隐式转换成指针常量:
int arr[5] = {0, 1, 2, 3, 4, }; cout << "arr[2] = " << arr[2] << endl; //2 cout << "*(arr + 2) = " << *(arr + 2) << endl; //2 //实际上,arr[2]就是被隐式转换成了*(arr + 2)
-
-
但是在大部分情况下,也有例外:
-
对数组名使用sizeof运算符时,会得到整个数组所占内存的大小
int arr[10] = {0}; cout << "sizeof(arr) = " << sizeof(arr) << endl; //sizeof(arr) = 40 10(数组大小) * 4(int所占字节数)
-
对数组名使用取地址符时,得到的是数组的地址(虽然和数组首地址的值一样,但是两个不同的概念)
int arr[10] = {0}; cout << "&arr = " << &arr << endl; //&arr = 0x7ffe9454f800 cout << "arr[0] = " << arr[0] << endl; //&arr = 0x7ffe9454f800 cout << "arr = " << arr << endl; //arr = 0x7ffe9454f800
-
注意 数组的地址 和 数组首地址 的区别
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, }; cout << "sizeof(arr) = " << sizeof(arr) << endl; //40 cout << "&arr = " << &arr << endl; //&arr = 0x7ffcd2be1270 cout << "&arr + 1 = " << &arr + 1 << endl; //&arr + 1 = 0x7ffd5b0b0428 这里是首地址加了40,指向了arr的最后一个元素的下一个元素(尾后指针) cout << "arr = " << arr << endl;//rr = 0x7ffd5b0b0400 cout << "arr + 1 = " << arr + 1 << endl; //rr = 0x7ffd5b0b0404
-
-
使用sizeof获取数组元素个数
int arr[] = {1, 2, 3, 4, 5, 6, 7, 0, }; cout << "sizeof(arr) / sizeof(arr[0]) = " << sizeof(arr) / sizeof(arr[0]) << endl; //8
14.A和W
- A就是用的ANSI编码,这种根据当前操作系统自动识别使用的编码的,W就是UTF16LE编码,一般用W
- “你好”、L"你好"
- 前面那个就是ANSI编码,也有一个说法叫多字节编码
- 后面那个就是UTF16LE编码,说法就是宽字节编码
15.常函数
-
const修饰类的成员函数,一般放在函数体后
-
void funcName(int a, ...) const;
-
常函数的实现也要带const关键字
-
不能修改类的成员变量
-
不能调用类中没有被const修饰的成员函数(只能调用常成员函数)
17.cout和wcout
- 字符集:字符的数字代码集。ANSI/ASCII、MBCS(Multibytes)、Unicode等
- 编码方案:记录字符代码的方式。UTF-8、UTF-16、GB2312等,分为“变长编码”与“定长编码”
- GB2312编码 — GB2312字符集+GB2312编码方案。两字节定长编码
- Unicode编码 — Unicode字符集+UTF-X编码方案。UTF-16为两字节定长编码,UTF-8是变长编码,为了兼容工业应用中已有的ANSI/ASCII编码设计的
- 源码文件编码是文本文件保存时指定的编码。常量字符串在代码中是用源码文件编码记录的。
- 程序文件编码依赖系统环境设置
17.1 cout
- 流操纵算子(格式控制符)或成员函数控制输出格式
- 与<< 和 cout 连用即可
流操作算子(成员函数) | 作用 | 流操作算子(成员函数) | 作用 |
---|---|---|---|
dec | 默认,十进制输出整数 | hex | 十六进制输出整数 |
oct | 八进制输出整数 | fixed | 普通小数形式输出浮点数 |
scientific | 科学计数法输出浮点数 | left | 左对齐,宽度不足将填充字符添加到右边 |
right | 默认,右对齐,宽度不足填充字符添加到左边 | setbase(b) | 设置输出整数时的进制,b=8/10/16 |
setw(w) | 输出/输入宽度为w个字符 | setfill© | 宽度不足时用c填充(默认空格) |
setprecision(n) | 在使用非fixed且非scientific方式输出的情况下,n为有效数字最多的位数,如果超过n,小数部分四舍五入或自动变为科学计数法,输出并保留以供n为有效数字。fixed方式和scientfix方式下,n是小数点后面应保留的位数 | setiosflags(flag) | 将某个输出格式标志置为1 |
resetiosflags(flag) | 将某个输出格式标志置为0 | boolapha | 把true和false输出为字符串 |
noboolalpha | 默认。把true和false输出为1和0 | showbase | 输出表示数值的进制的前缀 |
noshowbase | 默认。不输出表示数值的进制的前缀 | showpoint | 输出小数点 |
noshowpoint | 默认。只有小数部分存在才显示小数点 | showpos | 非负数值中显示+号 |
nonshowpos | 默认。非负数值中不显示+号 | skipws | 默认。输入时跳过空白字符 |
noskipws | 输入时不跳过空白字符 | uppercase | 十六进制中使用A~E。输出前缀则输出0X,科学计数法输出E |
nouppercase | 默认。十六进制使用a~e。输出前缀则输出0x,科学计数法输出e | internal | 数值的正负号在指定宽度内左对齐,数值右对齐,中间由填充字符填充 |
-
setiosflags()算子实际上是一个库函数。flag标志可以是如下值:
-
标志 作用 标志 作用 ios::left 输出数据在本域宽范围内左对齐 ios::right 输出数据在本域宽范围内右对齐 ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充 ios::dec 设置整数的基数为10 ios::oct 设置整数的基数为8 ios::hex 设置整数的基数为16 ios::showbase 强制输出整数的基数 ios::showpoint 强制输出浮点数的小数点和尾数 ios::uppercase 以科学计数法E 和 以十六进制输出字母时以大写表示 ios::showpos 对整数显示+号 ios::scientific 浮点数以科学计数法格式输出 ios::fixed 浮点数以定点格式(小数)输出 ios::unitbuf 每次输出后刷新所有流 ios::stdio 每次输出后清除stdout,stderr -
多个标志位可以用 | 运算符链接,表示同时设置:
cout << setiosflags(ios::left | ios::showbase | ios::stdio) << endl;
18.atio
-
int atoi(const char *str);
-
把字符串转成整数。会扫描参数中的所有字符串,跳过前面的空白字符
-
如果不能转为int或字符串本身为空,返回0。注意如果字符串前面是数字,后面为非数字,前面的数字会被转换成功,反之非数字在前就会失败
-
#include<iostream> #include<algorithm> #include<string> using namespace std; int main(){ string s1 = "1234"; string s2 = "1234abcd"; string s3 = "abcd1234"; string s4 = "abcd"; cout << "1234 = " << atoi(s1.c_str()) << endl; //1234 cout << "1234abcd = " << atoi(s2.c_str()) << endl; //1234 cout << "abcd1234 = " << atoi(s3.c_str()) << endl; //0 cout << "abcd = " << atoi(s4.c_str()) << endl; //0 return 0; }
17.2 wcout
-
在缺省的C locale下,wcout不可以直接输出中文,需要将locale设为本地语言才能输出中文
wcout.imbue(locale(locale(), "", LC_CTYPE)); //或者 wcout.imbue(locale("", LC_CTYPE)); //或者 wcout.imbue(locale("")); //这个会改变wcout的所有locale设置,不建议使用。 wcout << L"11223344:" << 11223344 << endl; //11223344:11,223,344 //或者 wcout.imbue(locale("chs")); //指定chs即MS936 CodePage映射,系统将UTF-16码流转为GB2312码流输出到控制台焕春
18.ends和flush
-
ostream& ends (ostream& os); template <class charT, class traits> basic_ostream<charT,traits>& ends (basic_ostream<charT,traits>& os);
_CRTIMP inline basic_ostream<char, char_traits<char> >& __cdecl ends(basic_ostream<char, char_traits<char> >& _O) { _O.put('\0'); return (_O); }
-
ends实际上是输出了一个’\0’字符
-
在C++使用ends时,都是在缓冲区插入’\0’,但不会马上刷新缓冲区。但是在Windows下会输出空格,Linux下则什么也不显示。这是两个系统对’\0’处理方式不同造成的
-
使用flush可以直接刷新缓冲区而不额外输出
19.assert宏
-
一个用于调试程序的宏,仅在DEBUG模式下生效,Release模式下会被自动忽略
-
#include <assert.h> void assert(int expression);
-
用于在函数开始时检验传入的参数的合法性。判断expression是否为真,如果结果为假,输出诊断消息并终止程序(向stderr打印错误信息,调用abort终止程序),避免导致严重后果以及方便查找错误
-
缺点:频繁调用会极大影响程序性能,增加额外开销。无需调试时应使用#define NDEBUG
#define NDEBUG #include <assert.h> ...
-
注意事项:
- 每个assert应该只检验一个条件,以便直观地判断是哪个条件导致失败
- 不应在assert中传入任何改变环境的语句,因为它仅是一个调试宏,禁用它或者它在另一种模式下被忽略时这个语句将会失效
- 应该使用assert对函数的参数进行确认
- 主要使用assert完成“假定”的检查以及“不可能发生的事情”发生了时的报警
20._tcscpy代替strcpy、wcscpy、lstrcpy等
-
使用_tcscpy时,不论时unicode编码还是其他编码,都无需求改代码
-
以上函数功能皆为:复制一个字符串到缓冲区
-
const TCHAR DirPath[300] = "abcdefg"; TCHAR szTemp[305] = { 0 }; //_tcscpy_s(szTemp, 305, DirPath);//第二个参数是元素个数并非字节数,直接写305只是在TCHAR为char时恰好正确,使用Unicode编码时会出错 //安全拷贝函数,DirPath的长度不能超过305-1,将DirPath拷贝到szTemp中,自动在szTemp后加上'\0' _tcscpy_s(szTemp, sizeof(szTemp) / sizeof(szTemp[0]), DirPath);
21.
21._tcslen、_tprintf
- _tcslen()是wcslen()和strlen()是通用类型。
- 如果是Unicode,就是wcslen()
- 如果是多字节编码,就是strlen()
- _tprintf() 是 printf() 和 wprintf() 的通用类型;
- 如果定义了 _unicode,那么 _tprintf() 就会转换为 wprintf(),
- 否则为 就会转换为printf()
22.sprintf、snprintf
22.1 sprintf
-
int sprintf(char *buffer, const char *format[, argument] ...);
-
与printf类似,只是printf输出到控制台,而sprintf输出到字符串
-
返回值:成功则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。失败则返回一个负数。
22.2 snprintf
-
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
-
最多从原字符串中拷贝n-1个字符到目标串中,然后再在后面加一个’\0’。如果目标串的大小>=n,不会溢出,还是取n-1个字符。
-
返回值:成功返回想要写入的字符串长度(不包括’\0’),出错返回负值
23.to_string(to_wstring)
-
C++11新标准引入。C++11以前可以使用:
- cstdlib库中的 itoa()
- sprintf()、snprintf()以及相关的几个安全函数等
- stringstream.str()
-
将以下参数中的各个标准类型的数据转为string类型(to_wstring同理)
-
string to_string (int val); string to_string (long val); string to_string (long long val); string to_string (unsigned val); string to_string (unsigned long val); string to_string (unsigned long long val); string to_string (float val); string to_string (double val); string to_string (long double val);
-
返回一个包含val作为字符序列的表示形式的字符串对象。使用格式与printf为相应类型打印格式相同
24.类型选择规则
参考自 C++ Primer(第五版)
- 数值明确不为负数时使用无符号类型
- 使用int执行整数运算。short太小,long一般和int一样大。超过int就用long long
- 算术表达式不适用char或bool。因为char在一些机器上是有符号的,一些机器上是无符号的。如果需要一个不大的整数,使用signed char 或者 unsigned char
- 浮点数运算用double。float通常精度不足,且单精度和双精度的计算代价相差不大,甚至在某些机器上双精度更快。
- long double提供的精度一般情况下没有必要,它的运行时消耗也比较大
25.extern
1. 进行链接指定(extern “C”)
-
C++ 为了解决多态问题,解析的时候会把函数名和参数合在一起生成一个中间函数的名称,C语言则不会,二者的函数名修饰方式不同,编译时会报错 “unresolved external symbol xxx”
-
因此在C++中引用C语言的函数和变量、包含C语言的头文件时,需要使用extern "C"以告诉编译器保持我原来的名称,不要生成用于链接的中间函数名
-
extern "C" void test();
2. 标示当前变量或函数的定义在其他模块中寻找
-
C++支持分离式编译,很多时候都将声明和定义区分开来,声明使名字为程序所知,定义负责创建与名字关联的实体。
-
而一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。如果想声明一个变量而非定义它,就在变量名前加extern关键字
-
代码演示:
//testExtern.h #pragma once //要在除了testExtern.cpp之外的文件使用这个arr,就必须声明extern extern char* arr; class A { public: A() {} ~A() {} void testShow(); };
//testExtern.cpp #include "testExtern.h" #include <iostream> #include <string> //定义,未赋值未初始化,直接访问也会报错 //char arr[] = "abcd"; //这和.h文件中extern的char *是两个变量 某类型的数组 != 某类型的指针 char* arr = new char[50]; //如果没有定义会编译报错:无法解析外部符号 char * arr void A::testShow() { memset(arr, 0, 50); snprintf(arr, 50, "test char *"); std::cout << arr << std::endl; }
//main.cpp #include "testExtern.h" #include <iostream> extern char* arr; int main() { std::cout << arr << std::endl; //访问时未初始化,为乱码 A a; a.testShow(); std::cout << arr << std::endl; //和testShow结果一样 system("pause"); return 0; }
26.函数
26.1 函数基础
-
典型的函数定义包括:返回类型、函数名字、由0或多个形参组成的列表、函数体
-
通过调用运算符 “()” 来执行函数,它作用于一个表达式,该表达式是函数或者指向函数的指针
-
“()” 之内是实参列表,用实参初始化函数形参。实参类型必须与对应的形参类型匹配
-
#include <iostream> //n的阶乘,非递归 int func(int n){ //n 是形参 int res = 1; while(n > 1){ res *= n--; //n * (n - 1) * (n - 2) * ... * 1 } return res; // ①返回语句中的值;②将控制权从被调函数转回主调函数 } int main(){ int n; std::cin >> n; // 函数的调用:①实参初始化函数对应的形参;②将控制权转移给被调用函数,主调函数的执行被暂时中断,被调函数执行 int res = func(n); //调用函数,传实参 std::cout << n << "! = " << res << std::endl; system("pause"); return 0; }
-
特殊的返回类型:void,表示函数不返回任何值
-
函数返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针
26.1.1 局部对象
- 局部变量:形参和函数体内部定义的变量,仅在函数的作用域内可见,同时还会隐藏在外层作用域中同名的其他所有声明中
- 此类对象在程序启动时被创建,直到程序结束才会被销毁
- 局部变量的生命周期依赖于定义方式
自动对象:
- 只存在于块执行期间的对象
- 当块的执行结束,块中创建的自动对象的值就变为未定义
- 形参是一种自动对象,函数开始时为形参申请存储空间,函数一旦终止,形参也被销毁
- 对于局部变量对应的自动化对象:
- 如果变量定义本身含有初始值,就用这个初始值初始化
- 变量定义本身不含初始值,执行默认初始化。即内置类型的未初始化局部变量将产生未定义的值
局部静态对象
-
当有必要让局部变量的生命周期贯穿函数调用及之后的时间时,可以将局部变量定义成静态类型(static)
-
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,知道程序终止销毁,在此期间即使对象锁在函数结束执行也不会对它造成影响
-
#include <iostream> int CountCalls(){ static int count = 0; //声明为静态的,调用结束后,这个值仍然有效,只会进行一次初始化 return ++ count; //会一直累加 } int main(){ int calls = 0; for(int i = 0; i < 10; ++i){ calls = CountCalls(); } std::cout << "Calls : " << calls << std::endl; //10 }
26.1.2 函数声明(函数原型)
- 函数的名字必须在使用前声明
- 函数的定义只能有一个,但声明可以有很多个
- 如果一个函数永远也不会被使用,那么可以只有声明没有定义
- 函数的声明不包含函数体,只需用一个分号代替;也无须形参的名字,只有形参类型即可
- 建议在头文件中进行函数声明,在源文件中进行函数定义,定义函数的源文件只需把含有函数声明的头文件包含即可
26.1.3 分离式编译
- 多文件时,如果修改了其中一个源文件,只需要重新编译改动了的那个文件
- 大多数编译器提供了分离式编译每个文件的机制,这一过程会产生一个后缀名是 .obj(WINDOWS) 或 .o(UNIX)的文件
- 后缀名的含义是该文件包含对象代码
多文件编译执行过程
- .cpp文件
- 每个cpp文件之间相互独立,一个编译单元指的是.cpp和其中包含的.h文件
- .h文件中的代码会扩展到包含它的所有.cpp文件中
- 编译器编译各个.cpp文件为.obj文件
- 编译器完成工程里所有的.cpp文件的编译后,再由链接器进行链接成为一个.exe或.dll文件
- .obj文件
- 目标文件,是程序经过编译后生成的,不能直接执行,需要链接程序后才能生成可执行文件
- 一般由机器代码组成,有一些是由自己定义的一些伪指令代码组成
- 链接程序是把目标代码和它所使用的库文件链接的程序(obj给出程序的相对地址,exe给出绝对地址)
- 目标文件类型:
- 可重定位文件(.o 或 .obj):包含由适合于其他目标文件链接来创建一个可执行的或共享的目标文件的代码和数据
- 共享的目标文件(库文件):
- 静态库(静态链接程序):目标文件集合。链接时,链接器将从库文件取得所需代码复制到生成的可执行文件中,和程序运行时没有关系
- 动态库(动态链接程序):程序运行时由系统动态加载到内存中供程序调用,只需载入一次,不同的程序可以得到内存中相同的动态库副本,节省内存
- .exe文件
- 可执行文件。
- 可以呗操作系统创建一个进程来执行的文件
.o文件在编译后就能获得,但是库文件,可执行文件都需要在链接后才能获得
源代码–>编译器–>汇编代码–>汇编器–>目标代码–>链接器–>可执行程序
编译:读取源程序(字符流) --> 进行词法语法分析 --> 转换高级语言指令为汇编代码 --> 转换为机器码 --> 生成目标文件(.obj / .o)
- 预处理阶段:宏(#define)、条件编译指令(#ifdef等)、头文件(#include)、特殊符号(LINE、FILE)
26.2 参数传递
- 如果形参是引用类型,则将它绑定到对应的实参上(即引用形参是它对应的实参的别名);否则将实参的值拷贝后赋给形参
26.2.1 指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针,因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
#include <iostream>
void reset(int* p){
*p = 100; //修改的是ip所指对象的值
p = 0; //p的地址为0,只改变了形参
std::cout << "p = " << p << std::endl;
}
int main(){
int a = 0, b = 20;
int* p = &a; //指针p指向a的地址
int* q = &b; //指针q指向b的地址
*p = 10; //a的值改为10,指针p的值不变(还是指向a的地址)
p = q; //指针p和指针q一样指向了b,但是指针q和变量b的值都不变
reset(p);
std::cout << "p = " << p << std::endl; //这里p的地址还是和q一样
std::cout << "*p = " << *p << std::endl; // 100
return 0;
}
C程序员常使用指针类型的形参访问函数外部的对象。C++中建议使用引用类型的形参代替指针
26.2.2 传引用参数
引用的操作实际上是作用在引用所引的对象上,也就是给原本的对象起了个别名:
#include <iostream>
void reset(int& p){ //使用引用无需拷贝,提高效率
p = 0;
}
int main(){
int a = 0, b = 20;
int& p = a; //引用变量p绑定了变量a,相当于给a起了个别名为p
p = 100;
std::cout << "p = " << p << std::endl; //100
std::cout << "a = " << a << std::endl;
p = b;
std::cout << "p = " << p << std::endl; //20
std::cout << "a = " << a << std::endl; //20
reset(p);
b = p;
std::cout << "p = " << p << std::endl; //0
std::cout << "a = " << a << std::endl; //0
std::cout << "b = " << b << std::endl; //0
return 0;
}
如果函数无需改变引用形参的值,最好将其声明为常量引用
常使用引用形参返回额外信息:一个函数只能返回一个值,但有时候函数需要返回多个值,这种时候除了定义一个新的数据类型包含多个成员之外,还可以给函数传入一个额外的引用实参:
int GetCount(const char ch, const std::string str, std::string& ss){
int count = 0;
for(int i = 0; i < str.size(); ++i){
if(str[i] == ch){
++count;
continue;
}
ss += str[i];
}
return count;
}
int main(){
std::string str = "aaabbbccddeeff";
std::string ss = "";
int count = GetCount('a', str, ss);
std::cout << "count = " << count << std::endl //3
<< "ss = " << ss << std::endl; //"bbbccddeeff"
return 0;
}
26.2.3 数组形参
-
不允许拷贝数组,所以无法以值传递的方式使用数组参数
-
使用数组时通常会将其转换成指针,所以当为函数传递一个数组时,实际上传递的是指向数组首元素的指针,以下三个函数尽管形式不同,但是是等价的:
void print(const int*); void print(const int[]); void print(const int[10]); int i = 0; int arr[2] = {0, 1}; print(&i); //正确,&i的类型的int* print(arr); //正确,j转换为int*并指向j[0]
#include <iostream> // C风格字符串,使用标记指定数组长度 void print(const char* cp){ if(cp){ while(*cp){ std::cout << *cp++ << " "; } } } int main(){ std::string str = "abcdef12345"; print(str.c_str()); return 0; }
-
**数组引用形参:**引用两端的括号必不可少
void func(int (&arr)[10]){ std::cout << arr[0] << std::endl; } // void func1(int &arr[10]){ //error-type: 不允许使用引用的数组C/C++(251) // std::cout << arr[0] << std::endl; // }
-
传递多维数组:
void func2(int (*arr)[10]){ //指向含有10个整数的数组的指针 std::cout << arr[0][0] << std::endl; } void func3(int *arr[10]){ //10个指针构成的数组 std::cout << arr[0][0] << std::endl; } int main(){ int arr[10][10] = { {1, 2, 3}, {4, 5, 6}, }; func2(arr); //func3(arr); //"int (*)[10]" 类型的实参与 "int **" 类型的形参不兼容C/C++(167) return 0; }
26.2.4 含有可变形参的函数
-
处理不同数量实参的函数,C++11提供了两种主要方法:
- 所有实参类型相同 — 传递一个名为initializer_list 的标准库类型
- 实参类型不同 — 编写可变参数模板
-
initializer_list形参:如果函数的实参数量未知但是全部实参类型相同,可以使用它,它用于表示某种特定类型的值的数组
initializer_list<T> lst; //默认初始化,T类型元素的空列表 initializer_list<T> lst{a, b, c...}; //lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const lst2(lst); lst2 = lst; //拷贝或赋值一个initializer_list对象,不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
27.STL(待补充)
4.1 set、map存储结构体
-
C++中的set和map是红黑树实现的,要使用自定义类型的时候要在类型中重载小于号”<“。即要对自定义类型有偏序关系,这样C++才能正确构建红黑树
-
struct Test { int x; string y; double z; //比较运算符重载,按位置排序,必须const bool operator <(const Test& a) const { return x < a.x; } }; //方法二 bool operator<(const Test& a, const Test& b) { return a.x < b.x; }
4.2 遍历删除元素
void testDelete() {
unordered_map<int, float> m2;
m2.insert({ 1, 1.5 });
unordered_map<char, string> m3;
m3.insert({ 'a' , "aaa" });
unordered_map<int, set<Test*>> m1;
Test t1 = { 1, "1", 1.5 };
Test t2 = { 2, "2", 2.5 };
Test t3 = { 3, "3", 3.5 };
set<Test*> test;
test.insert(&t1);
test.insert(&t2);
test.insert(&t3);
// m1.insert(make_pair(2, test));
m1.insert({ 2, test });
//m1.emplace(1, test); //emplace避免产生不必要的临时变量。这里直接用insert会报错:没有与参数列表匹配的重载函数
//遍历删除元素(vector/list/map/set/deque/string等同理)
for (unordered_map<int, set<Test*>>::iterator iter = m1.begin(); iter != m1.end(); iter++) {
for (set<Test*>::iterator i = iter->second.begin(); i != iter->second.end(); /*如有插入/删除操作,这里不能递增或递减迭代器*/) {
cout << (*(i))->x << endl;
cout << "Erase " << (*(i))->x << endl;
iter->second.erase(i++); //必须在删除后立即进行递增或递减操作
//i = iter->second.erase(i++); //或者使用迭代器去接收移除元素后返回的结果(erase会返回紧随被删除元素的下一个元素的有效迭代器)。推荐这种写法
//--i; //运行时错误,不能减少值初始化的map/set迭代器
}
cout << "size: " << iter->second.size() << endl;
}
}
二、一些项目错误记录与总结
1.vs报一些莫名其妙的错误问题
ErrorList 中报错但是实际上代码没有问题:
- 检查上下文,注意如果是.h和.cpp分开的两个文件,头文件中没有定义的库函数中的类型/结构体等不会报错,但实际上是有问题的,所以相关头文件不能写在.cpp中
- 单独创建的项目中写的类,头文件分开包含(即分别按需写在了.h和.cpp中)不会报错。但是整合到一个大的工程项目中时出现乱报错问题。个人尝试是把添加的模块代码的类的代码的头文件全部改为了写在.h中,.cpp中不添加任何其他头文件,编译成功,这应该是头文件冲突问题,在大项目中应该要非常注意,否则很容易出现无法预知的错误。
2.使用API时函数无法获取系统路径下相关信息
- 可以考虑是否是32位与64位问题
- 我尝试使用windows api 获取数字签名,但在使用CryptQueryObject()函数时发现无法获取系统根目录下的数字签名信息,会返回错误码表示无法找到申请对象
- 如无位数限制,可以修改项目为64位编译,就可以获取到所有系统信息
3.编译报错 unresolved external symbol xxx
- 检查使用相关库时是否需要说明静态库调用:
- 在 .cpp上使用 #pragma comment(lib, “xxx.lib”) 说明静态库(工程文件夹下有此 .lib文件)
- 在VS中选择当前项目的属性 – Linker – General – Additional Library Directories – 相关lib文件路径 且 Linker – input – Additional Dependencies – 添加 xxx.lib (lib的名称)
- 如果当前文件为DLL文件,编译后的函数供其他程序调用,则需要检查DLL文件中的函数是否添加了 extern “C”。具体原因参照”C和C++函数名修饰“
4.可执行程序在不同的机器上可能崩溃
- 检查是否可能触发空指针的情况
- 检查是否调用了相关释放指针的函数,但是指针已经进行了移动(例如在循环中进行 ++操作的指针)然后又进行了释放,这肯定是错误的。要释放此类型的指针应该在最开始用一个temp存放这个指针最开始的位置,最后再释放temp
5.1.C3859 未能创建PCH虚拟内存
属性 — C/c++ — 命令行 — 添加 /Zm1000(或者更大的数)
- /Zm 指定预编译头的内存分配限额 确定编译器分配的用于构造预编译头的内存量
- 数值:
- 10 7.5MB
- 100 75MB
- 200 150MB
- 1000 750MB
- 2000 1500MB
6.导入整体项目显示无法打开文件xxx.lib
- 单个项目解决方案(1和2选一点即可)
- 项目属性 — C/C++ — 常规 — 附加包含目录 — 添加所需lib所在的文件夹路径
- 项目属性 — 链接器 — 常规 — 附加库目录 — 添加所需lib所在的文件夹路径
- 项目属性 — 链接器 — 输入 — 附加依赖项 — 添加lib的名称(eg: xxx.lib)注意不是路径
- 单个项目能编译通过,项目整体不行
- 按照项目顺序对整个文件夹/单个项目编译
- 找到出错的一个小整体,对其中的进行单个项目编译,单个项目编译出错参考第一条解决
- 最后在VS工具栏选择生成 — 生成解决方案(不要重新生成)
- 可能的原因:项目之间生成的lib库存在依赖,整个项目本身没有调整好项目之间的顺序。全部生成时可能存在覆盖问题,即按照编译器既定的顺序编译时可能会存在先删除已有的再重新编译的情况。