C++回炉重造

本文目录

  • 关于函数
  • 类与对象
  • Visual Studio的使用
  • 数据结构
  • 编程中要注意的一些小问题
  • 手动编译
  • 小技巧
  • 疑难杂症

getch()与getchar()的区别
getchar跟getline比较类似,我们输入一串字符串,然后回车,它才开始处理。它将回车识别为换行符\n
getch是每按下一个按键就会处理一次,也就是不是从输入缓冲区读取的数据。它将回车识别会回车符\r。因此getch可以用来实现输入密码时的文本隐藏,而getchar不行。

使用高版本VS编译时经常遇到的各种C4996提示
strcatstrcpy:存在缓冲区溢出等问题,没有检查结果会不会超长,完全信任程序员,容易翻车,因此改用strcat_sstrcpy_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接口、功能都是一样的,只是换了个名字。

coutprintf的格式控制

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自己的构造函数

Visual Studio的使用

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的相互关系:gccg++编译单个文件,make根据我们写的makefile编译一批文件,cmake根据我们写的CMakeLists.txt生成makefile,因此小项目用gcc或g++就够了,大一点的项目用make,更复杂的项目就要cmakemake一起上了。
首先复习一下编译和链接的关系:看这里。编译是把我们自己写的代码转换成二进制目标文件.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 -j44核编译
make -j$((`nproc`-2))使用系统拥有的核心数-2个核进行编译,自适应设备,减1到2是留一点给其他人,不至于完全卡死
make installmake之后会得到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(常见于本地测试用例跑过了一提交却是编译错误):

  1. 嵌套容器,windows写法随意,g++会把>>当做别的东西,报error: ‘>>’ should be ‘> >’ within a nested template argument list,因此要把>>写成> >

小技巧

在调试代码的过程中有时候需要手动输入一长串数值,调试的次数多了打得也累,为了偷懒,我们可以使用./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 | grep "not found",没有输出就是正常的。

疑难杂症

  • 重载<<运算符时报错C2280:尝试引用已删除的函数,原因是参数表里的ostream对象忘记加引用了,ostream不支持拷贝构造,只能通过引用传参。
  • 使用g++编译的时候报错collect2.exe: error: ld returned 1 exit status,原因是编译的文件里头没有main()入口函数,所以要补上一个main(),参考链接

你可能感兴趣的:(c++)