本文目录
getch()与getchar()的区别
getchar
跟getline比较类似,我们输入一串字符串,然后回车,它才开始处理。它将回车识别为换行符\n
;
getch
是每按下一个按键就会处理一次,也就是不是从输入缓冲区读取的数据。它将回车识别会回车符\r
。因此getch
可以用来实现输入密码时的文本隐藏,而getchar
不行。
使用高版本VS编译时经常遇到的各种C4996提示
strcat
跟strcpy
:存在缓冲区溢出等问题,没有检查结果会不会超长,完全信任程序员,容易翻车,因此改用strcat_s
和strcpy_s
,增加一个参数指定目标存储空间的长度,如果超过拷贝字符数量就会报错(参考链接1,参考链接2):
char szBuf[3] = {0};
// 拷贝一个长度超过dst的容量的字符串
strcat(szBuf, "kdfdfj"); // 不会报错,可能影响其他程序使用的数据
strcat_s(szBuf, 3, "kdfdfj"); // 指明szBuf的长度为3,如果src长度超过3就会报错,因此这里会报错
getch
:微软决定把一些函数名称让给程序员,为了不覆盖掉原来的库函数,C++自带的部分库函数加上了下划线,比如_getch
,于是我们就可以写一个自己的getch
函数,而不会互相冲突。_getch
跟原来的getch
接口、功能都是一样的,只是换了个名字。
cout
和printf
的格式控制
setw对接下来的一个内容设置输出宽度
using
关键字
// 之前用typedef定义链表结点类型
typedef struct LinkNode {
elemType data;
struct LinkNode* next;
};
// 不过去掉typedef在VS2019中也能正常工作,不是很懂
struct LinkNode {
elemType data;
struct LinkNode* next;
};
// 尝试过这样写,会报错,没看很懂,貌似编译器认为using里的LinkNode跟struct里的
// LinkNode是两个东西,所以重定义了,不行
using LinkNode = struct {
elemType data;
struct LinkNode * next;
};
// 用这种写法就没有问题了,右边是一个完整的struct声明
using LinkNode = struct _LinkNode{
elemType data;
struct _LinkNode* next;
};
// struct的后面一定要有一个标识符,就是定义了一个结构类,这个类的名字。不管前面有没有
// using,这个标识符是可以直接用在正文里头的,所以它也不能跟其他struct声明同名
offsetof
函数用于计算成员在对象中的偏移量(字节),利用这个函数我们可以用寻址的方式,已知struct
的起始地址就能找到某个数据成员的地址,也可以反过来已知数据成员的地址找到struct
的起始地址,参考链接
多文件编译
VS多文件编译老是出重定义问题,查了一下,根据参考链接,#define
的作用域仅仅是单个.cpp
,而不是全局所有的.cpp
文件,所以在单个cpp
或.h
文件里多次#include
同一个头文件能如预期的只编译一次,但多个.cpp
文件都#include
同一个头文件时,这样会出问题。问题是类外定义的非static及非inline函数还是会报LNK200: xx函数已经在yy.obj中定义
。参考链接的作者给出的解决方案是:在头文件中只进行声明,在一个同名的.cpp文件中再写定义,我还不知道为什么这样就会有效,也还没有去尝试,先写在这里
函数调用必须在函数块里,不能在全局作用域里面调用,那样只会被当做是在声明函数。
当我们在多个cpp
文件里声明了多个函数,又不想让A文件看到B文件里的函数,因为这样可能会触发重命名错误,此时可以用static
关键字修饰函数(放在最前面),表示这个函数只有本文件可以调用,其他文件看不到这个函数。
关于访问权限:
public
:无限制,在哪里都可以访问
private
:只在自己类的内部可以访问(当然也可以通过友元)
protected
:跟private
的唯一区别是在子类的内部也可以访问父类的protected
成员
C++的访问控制是类层面的(class-level), 而不是对象级别的(object-level),在类的成员函数内部可以访问同类实例的私有数据成员,而不用考虑具体是哪个实例(参考链接)。在类内定义运算符重载函数时,可以访问同类的另一个实例的私有数据成员,而不能访问不同类的另一个实例的私有数据成员,就是这个原因:
// 规则:
// 一斤牛肉:2斤猪肉
// 一斤羊肉:3斤猪肉
Pork Cow::operator+(const Cow &cow)
{
int tmp = (this->weight + cow.weight) * 2;
return Pork(tmp);
}
Pork Cow::operator+(const Goat& goat)
{
// 不能直接访问goat.weight
//int tmp = this->weight * 2 + goat.weight * 3;
int tmp = this->weight * 2 + goat.getWeight() * 3;
return Pork(tmp);
}
当创建继承自父类A的子类对象B时, 构造函数的调用顺序:
B的静态数据成员的构造函数 -> A的构造函数 -> B的非静态数据成员的构造函数 -> B自己的构造函数
if语句块最好加上花括号,有时候虽然觉得只有一个语句不需要加,但是出问题的时候要debug,直接在语句块里写下了调试语句,却忘记补上花括号,就会导致运行结果不合预期,而且要想半天才能想起来是这个问题
在新版VS中,“win32控制台应用程序"合并到"Windows桌面向导"里头了,进去之后选择"控制台应用程序”,勾选"空项目",就跟旧版的一样了
调试功能
逐语句
是一个一个语句执行,在遇到函数的时候会进入函数继续逐语句执行,类似ipdb的s命令;
逐过程
是在当前界面上一个一个语句执行,遇到函数的时候一次就过去了,不会进到函数里面,类似ipdb的n命令;
跳出
是直接走到函数返回的位置,类似ipdb的r命令
调试——窗口——调用堆栈,可以在出现异常时看到类似python的traceback的信息,方便查错
在保存VS项目后重命名项目目录,再次打开会出现所有文件都提示找不到,打开失败的情况,此时需要在"解决方案资源管理器"(一般在最右边)中右击解决方案——重新生成解决方案,即可自动更新路径,问题解决。
模板的作用域是一个块,也就是一个函数定义或者类定义,后面就无效了,需要再声明一次模板参数
<<
运算符声明时在<<
后为何要多写一个
(代码见下)?详见C++中模板类使用友元模板函数
#include
using namespace std;
template < typename T >
class A
{
friend ostream &operator<< < T >( ostream &, const A< T > & );
};
template < typename T >
ostream &operator<< ( ostream &output, const A< T > &a )
{
output << "重载成功" << endl;
return output;
}
事先说明,以下插入删除下标均从0
开始。
(可能不准确,待核验)对于插入删除这种需要定位到待处理元素的前一个元素的操作,while
循环的表达式为while(p->next)
,从头结点开始循环,计数值从0开始;而对于查找这种需要正好定位到那个元素的操作,while
循环的表达式为while(p)
,从头结点的下一个结点开始循环,计数值也从0开始。
一般要有两个struct
类,一个是基本存储单元(如链表中的结点),一个负责统领整个数据结构(比如队列需要一个保存着头尾指针的struct
)。在链表中链表的基本存储单元跟头结点可以用同样的结构,所以看起来就像只需要一个struct
类,但这只是特例,链式队列的结点跟统领的结构就不一样了。需要注意的是,基本函数传参时不是传struct
类,也不是struct
类的指针,而是struct
类的指针的引用,举个例子,如果是要在函数里new
,只传指针的话就是形参进行了new
操作,实参其实并没有指向新开辟的存储单元,这样就会有问题。
数据结构初始化时记得统领指针自己本身也是需要new
的,我经常忘记这一步,结果就变成一直在空指针上操作。
(后记:顺序表、堆这样的顺序存储结构好像都是传的结构体的引用,没传指针的引用,还没搞清楚)
插入元素的时候,像链表、树这样的数据结构,元素都是new
出来的,所以传结点的时候参数一定是Node*
类型。
delete
之后指针所指的存储空间并不是无法访问,只是数值变成了随机值,还是可以像正常的情况那样做一些赋值操作,但这样的操作是非法的,delete
之后指针应尽快赋回NULL
,以防节外生枝。
如果在写代码的时候提示“取消对空指针的引用”,那很有可能是把判断指针为空的条件写反了,比如把if(node)
写成了if(!node)
头文件里不要写using namespace xxx;
,参考链接
gcc、g++、make、cmake的相互关系:gcc
跟g++
编译单个文件,make
根据我们写的makefile编译一批文件,cmake
根据我们写的CMakeLists.txt生成makefile,因此小项目用gcc或g++就够了,大一点的项目用make
,更复杂的项目就要cmake
跟make
一起上了。
首先复习一下编译和链接的关系:看这里。编译是把我们自己写的代码转换成二进制目标文件.o
或.obj
,链接是将所有的目标文件以及系统组件组合成一个可执行文件.exe
。
将cpp编译成可执行文件
举个简单的例子比较一下,单个函数main.cpp计算阶乘:
#include
#include
#include
int factorial (int n)
{
if (n <= 1)
return 1;
else
return factorial (n - 1) * n;
}
int main (int argc, char **argv)
{
int n;
if (argc < 2)
{
printf ("Usage: %s n\n", argv [0]);
return -1;
}
else
{
n = atoi (argv[1]);
printf ("Factorial of %d is %d.\n", n, factorial (n));
}
return 0;
}
同一目录下CMakeList.txt中的内容为:
# 给定cmake版本最低要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名,此后可以通过 ${PROJECT_NAME} 使用项目名
project(example)
# 将一或多个源文件编译成一个可执行文件
add_executable(example example.cpp)
用g++编译是:g++ -o test main.cpp
,用cmake
则是:cmake -B build/ && cd build/ && make
,其中-B
是指定编译目录,makefile等文件会生成到这里;然后cd
进这个目录,进行一次make
操作,执行make
时当前目录下应有makefile文件,否则将无事发生。当然make
也可以指定makefile所在的目录,则命令变为cmake -B build/ && make -C build/
。不管哪种方式,都将在build/
目录下生成一个test可执行文件,我们运行./test 5
就会返回5的阶乘120。
将cpp编译成python可以调用的动态链接库
具体请看我的另一篇文章中对“在打包时添加c++文件拓展”一节的讲解。
cmake常用命令详解
make .
直接编译,单核
make -j4
4核编译
make -j$((`nproc`-2))
使用系统拥有的核心数-2个核进行编译,自适应设备,减1到2是留一点给其他人,不至于完全卡死
make install
在make
之后会得到so文件,而make install
则是更进一步,把根据CMakeList.txt把文件放到某个位置,make
称为"编译",make install
称为"安装"。
另外,CMakeList.txt也像cpp文件那样,可以通过include语句从外部文件导入一些变量,这些文件约定俗成使用.cmake作为后缀名,参考链接
使用不同版本的gcc进行编译
在实践中我们有时候会遇到模块比较多的情况,不同模块依赖的gcc版本可能不同,这就会导致一个gcc版本无法编译所有的内容。好在Ubuntu早就考虑到了软件不同版本的问题,针对这类问题,可以使用update-alternatives
进行管理,参考链接1,参考链接2
基本用法:
# 新建一个映射项名为name,将path所指的文件链接到link,优先级为priority
update-alternatives --install <link> <name> <path> <priority>
# 举个例子:
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100
# 查看某个项当前的配置情况
update-alternatives --display <name>
实际用法:
# 设置gcc-9为默认gcc编译器
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100
# 临时指定gcc-8为更高优先级
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 101
# 使用gcc-8进行编译
# 恢复gcc-9的优先地位
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 99
在ubuntu中用vscode编译调试C\C++
还有这篇
不知道为什么, 编译出来
编译:
g++ Container.h -o Container
使用system("PAUSE");
语句时g++编译报错: ‘system’ was not declared in this scope
解决方案: #include
VSCode中使用Ctrl+Shift+B
进行编译
使用visual studio的cl命令进行编译:
找到cl.exe所在的目录,并将其写入系统变量Path,在本机是
D:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\bin\Hostx64\x64
编译&链接命令是cl /EHsc test.cpp
,成功后将得到test.obj和test.exe,再运行.\test.exe
即可得到像VS中运行代码的效果
一开始通常会报这几种错:
fatal error c1034:不包括路径集;fatal error C1083: 无法打开某个.h文件;fatal error LNK1104: 无法打开某个.lib文件;
需要设置一些环境变量:解决方法
一些windows下跑得通但是linux的g++下会出编译错误的代码小bug(常见于本地测试用例跑过了一提交却是编译错误):
>>
写成> >
在调试代码的过程中有时候需要手动输入一长串数值,调试的次数多了打得也累,为了偷懒,我们可以使用./file.exe < input.txt
(windows下是.\file.exe
)来读取txt文件中的数据当做输入,然后我们只需要把所有想要测试的数据都写进txt文件中,再静静地观察测试结果就可以啦!不过奇怪的是,直接在bat文件中写入这个语句却貌似无法正常执行。需要注意的是,从文件读取会比我们在命令行窗口输入多了一个EOF,这个EOF在while(cin>>c)中会导致跳出循环,而同样的输入内容在调试窗口中则不会跳出循环(因为没有EOF)
如果想把输出信息也保存下来,只需多加一个参数,写成./file.exe < input.txt >output.ext
即可
ldd
可以查看一个so文件它所依赖的包:
# 截取了一部分ldd的输出
ldd libOpen3D.so
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f238428c000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f23840bf000)
libc++.so.1 => not found
libGL.so.1 => /usr/lib/x86_64-linux-gnu/libGL.so.1 (0x00007f2384036000)
如果有的依赖包找不到,就会报not found,此时so文件是不可用的,会报一堆unreference错误。
快捷版命令:ldd
,没有输出就是正常的。
<<
运算符时报错C2280:尝试引用已删除的函数,原因是参数表里的ostream
对象忘记加引用了,ostream
不支持拷贝构造,只能通过引用传参。