联合编程;编译器
- 操作系统下的内存分配:程序怎么放到内存的?
- 定义和声明
- extern
- 变量的定义
- 如何在C++代码中调用C语言的函数
- 用extern 关键字,把函数声明成C语言风格
- #ifdef __cplusplus extern "C" #endif
- 头文件和源文件
- 头文件里带定义,被多次调用的问题
- 头文件引用 重复定义的问题
- LNK4042错误
- 创建自己的SDK——流程
- 创建自己的项目类型
- 单一性规则 One Definition Rule
- 转换单元
- 未定义行为
- 单一性规则 ODR
- 举例1:非inline类型
- 举例2:inline类型
- 举例3:inline类型
- 单一性规则 ODR
- (1)对于const类型的常量,在不同转换单元可以重复定义;
- (2)对于static类型的常量,在不同转换单元可以重复定义;对比inline类型
- 名称的链接属性
- #define 用法
- (0)配合#ifdef 来检测宏
- (1)C语言中经常用#define来定义常量
- (2)定义复杂表达式宏
- (3)#undef 取消宏定义
- 预处理指令逻辑
- #ifdef #ifndef #else #elif #endif
- 预定义宏
- 标准预定义标识符
- _ _ func_ _
- _ _ DATE_ _ , _ _ TIME_ _ , _ _ FILE_ _ , _ _ LINE_ _
- _ _ cplusplus
- MSVC的预定义宏
- assert
- static_assert C++17的新语法
操作系统下的内存分配:程序怎么放到内存的?
注:x86 32位windows
(1)全局变量区:全局变量、static关键字定义的变量
(2)代码区:其它代码部分
(3)堆区:new的内存空间
(4)栈区:局部变量
编译完成后,这四个是挨着的。放到内存中也是
默认情况下每个程序(线程)独占内存(虚拟内存)
定义和声明
只有返回类型+函数名称+形参,没有具体实现的写法,就是函数声明;
例子:
int add(int a, int b);//函数声明int main()
{cout << add(1, 2);//调用
}int add(int a, int b)//函数定义
{return a + b;
}
编译器翻译上面面代码时,如果没有函数声明,调用时就会找不到(更准确的来说是不知道add返回什么类型,需要什么类型的参数);
编译器只负责翻译,不负责运行;
函数具体的实现是在运行时体现;
而返回类型和参数类型等是编译时就必须要明确,比如要判断调用是不是可行的;
函数声明就是为了告诉编译器函数的接口,然后编译器就知道函数被调用得对不对。
如此说来 函数声明时的形参名称也是可以不写的
int add(int, int);//函数声明int main()
{cout << add(1, 2);//调用
}int add(int a, int b)//函数定义
{return a + b;
}
但最好写上,因为编译器不看,人得看。
补充:
相同的函数声明可以多次出现,因为声明只是给编译器看,不会说让运行时出现冲突。
int add(int, int);//函数声明
int add(int, int); int add(int, int);
int main()
{cout << add(1, 2);//调用
}int add(int a, int b)//函数定义
{return a + b;
}
extern
变量的定义
int a;
变量的声明
注:只在全局有效
extern int a;
如何在C++代码中调用C语言的函数
用extern 关键字,把函数声明成C语言风格
用法1:
extern “C” { int add(int a,int b); }
用法2:
extern “C” { #include “myC.h” }
注:
由于C++支持函数的重载,所以编译时会对函数名进行较为复杂的修改 C语言就很简单,比如我定义一个int add();编译器会处理成int
_add();
#ifdef __cplusplus extern “C” #endif
略
头文件和源文件
头文件 .h 放声明
源文件 .cpp .c 放定义
.c源文件里只能写c语言代码。
举例:
xx.c
#include
int main()
{
std::cout<<“xxx”;
}
//虽然引用了头文件,但由于这段代码在.c文件中,所以还是会报错
//放回到.cpp文件中可以正常运行
做一个自己的库,包含头文件和源文件;
头文件里面放声明,源文件里面放定义
用这个库的时候,引用一下头文件就可以了,原因在于,引用了头文件相当于把头文件里面的声明放到那个位置,那么那个位置后面的函数就可以去使用这些声明。而编译完成后,整个程序是包含了库的源文件的内容的,自然也就能够正常运行。
代码到程序的过程:
(1)源文件编译成.obj 或 .o
(2)再把这些目标文件链接到一起
(3)头文件不会主动参与编译过程,除非被引用;
头文件里带定义,被多次调用的问题
编译时报错:xxx已被定义
函数名不可重复,除非是重载函数,即使在不同文件中,只要在同一个工程,就不可重复(不包括被命名空间限制、类函数等情况)
变量名同样的不可重复。
解决方法:
(1)不在头文件里写定义
(2)带static关键字的静态函数和静态变量
注意:
例如:头文件被多次引用,其中有静态变量的定义;由于其只在本地有效的特点,这就相当于在不同地方定义了多个这样的变量;
比方说:头文件里写了static int a{100};那么多次引用得到的初值也都是100,一个地方修改值,不会影响另一个地方(引用得到的静态变量)的值
这里举个例子(不过还是很绕,反正就是那个意思):
//mlib.h
extern int a;
void aaa();//mlib.cpp
#include "mlib.h"
int a{ 100 };
void aaa()
{a++;
}
//main.cpp
#include "mlib.h"
int main()
{aaa();cout << a;
}
输出a:101
因为先调用了函数aaa,使得a的值加1;
而如果是静态的变量
//mlib.h
static int a;
void aaa();//mlib.cpp
#include "mlib.h"
void aaa()
{a++;
}
//main.cpp
#include "mlib.h"
int main()
{aaa();cout << a;
}
输出a:100
虽然这里也是先调用函数aaa,但mlib.cpp里的a和main.cpp里的a是不一样的
同时也可通过汇编代码看到两个a的地址是不一样的:
void aaa()
{...a++;
00631023 mov eax,dword ptr [a (063308Ch)]
cout << a;
00631008 mov eax,dword ptr [a (0633088h)]
(3)带inline关键字的内联函数
头文件引用 重复定义的问题
画个简图:
然后就会报错:
报错原因就在于这里头文件里的的定义
mlib.h
static int a{ 100 };
在头文件被引用时,相当于是把头文件里的内容放到引用处;那这里就的static int a就会被反复定义。
解决方法:
(1)#pragma once
#pragma once可以告诉编译器,只被引用一次,文件内生效
使用方法:在存在被重复引用的,带定义的,头文件中,加上#pragma once
例子:
//mlib.h
#pragma once
static int a{ 100 };
void aaa();//pch.h
#include "mlib.h"//main.cpp
#include "pch.h"
#include "mlib.h"
int main()
{aaa();cout << a;
}
局限:有的编译器不支持#pragma once
(2) #ifndef #define #endif
用宏定义的方法:
例子:
//mlib.h
#ifndef _HMLIB_
#define _HMLIB_
static int a{ 100 };
void aaa();
#endif //!_HMLIB_
//pch.h
#include "mlib.h"//main.cpp
#include "pch.h"
#include "mlib.h"
int main()
{aaa();cout << a;
}
第一次被引用时,#define _HMLIB_生效;第二次被引用,由于_HMLIB_已经被定义,所以ifndef HMLIB == false;不会进入这个范围造成重复定义;
效果和#gragma once 一样;
注意:要注意区分#ifndef和#ifdef;
#ifndef false == true;
#ifndef true == false;
而
#ifdef false == false;
#ifdef true == true;
LNK4042错误
C和C++源文件混用问题
问题1:
vs2022环境下,同时有两个main文件:main.c main.cpp
解决方法:文件名
创建自己的SDK——流程
(0)假设我用的vs2022
(1)写好头文件和源文件
mysdk.h
mysdk.cpp
(2)配置类型为静态库
属性页-》配置属性-》配置类型-》静态库.lib
(3)生成
得到
mysdk.lib文件
(4)把mysdk.h和mysdk.lib拿出来
再加上示例、说明书、文档等等。
(5)做成安装包
就做好了SDK
直接拿mysdk.h和mysdk.lib来用的流程(vs2022):
(1)包含头文件
配置属性-》包含目录-》添加mysdk.h
(2)包含库
配置属性-》库目录-》添加mysdk.lib
(3)引用头文件#include<mysdk.h>
(4)使用库文件
方法一:
#pragma comment(lib,“mysdk.lib”)
方法二:
属性配置-》链接器-》附加依赖项-》mysdk.lib
= = = = = = = = = = = = = = = = = = = = = = = = =
如此就可以使用库里面的函数了。
整个过程的目的就是:
把头文件放到编辑器知道的位置,然后方便引用;
把库文件给编译器,让编译器 编译的时候把库文件编译进来。
创建自己的项目类型
项目-》导出模板
得到zip压缩包
把这个压缩包放到visual studio 2022\ProjectTmplates中,就可以使用这个模板了
单一性规则 One Definition Rule
转换单元
.cpp,.c源文件把所引用的.j头文件合并后,称为一个转换单元
编译:
编译器将每一个转换单元生成对应的.obj对象文件
对象文件:
包含了转换单元的机器码和转换单元的引用信息(不在转换单元中定义的对象)
链接:
将各个对象文件链接起来,生成目标程序
不在转换单元中定义的对象:
比如对象文件A中包含了定义在其他转换单元的引用,那么就要去其他转换单元的对象文件中寻找这个引用的定义来建立链接;如果找不到,就会产生一个链接错误:
例如:
extern int add();
int main()
{std::cout << add();
}
注意:如果你的程序连编译都过不去,你是看不到LNK字样的链接错误的。
未定义行为
举例:
int a{ 2 };
std::cout << 2 * a++ + ++a * 2;
这里不是简单运算优先级的问题;
++a先运算就会导致前面一个a加1,整个结果也就不一样;
这样一个式子在不同编译器中的结果是不确定的;
这就属于是未定义行为;
更为准确的说法是:C++标准没有规定的行为,就是未定义行为
单一性规则 ODR
基本上来说:
任何变量、函数、类、枚举、模板、概念(C++20)在每个转换单元中都只允许 有一个定义;
非inline的函数或变量(C++17),在整个程序中,有且仅有一个定义;
举例1:非inline类型
//test.cpp
void show()
{
}//main.cpp
void show()
{
}
int main()
{
}
不管有没有引用、调用,只要是在同一个工程,这样的重复定义行为就是不允许的。
//注:声明可以多次声明
举例2:inline类型
//test.cpp
inline int a{ 100 };//main.cpp
inline int a{ 100 };
int main()
{std::cout << a << std::endl;
}
运行结果:100
对于inline类型,是每个转换单元只能有一个
举例3:inline类型
对于inline类型的 变量a,如果在不同转换单元中有重复定义,对变量进行的修改:
看示例:
//test.cpp
inline int a{ 100 };
void test()
{std::cout << "test.cpp中第一次打印:" << a << std::endl;a += 100;std::cout <<"test.cpp中第二次打印:" << a << std::endl;
}//main.cpp
inline int a{ 100 };
void test();
int main()
{a += 100;test();std::cout <<"main.cpp中打印:" << a;
}
输出:
test.cpp中第一次打印:200
test.cpp中第二次打印:300
main.cpp中打印:300
反汇编:
main(void)a += 100;
001E1DE6 mov eax,dword ptr [a (01ED008h)]
001E1DEB add eax,64h
001E1DEE mov dword ptr [a (01ED008h)],eax
test(void)a += 100;
001E6428 mov eax,dword ptr [a (01ED008h)]
001E642D add eax,64h
001E6430 mov dword ptr [a (01ED008h)],eax
可以看出,不管你在哪里修改,实际修改的都是同一个地址下的值;
inline修饰的变量具有外部链接属性, 作用范围是整个程序;
单一性规则 ODR
(1)对于const类型的常量,在不同转换单元可以重复定义;
//test.cpp
const int a{ 100 };//main.cpp
const int a{ 100 };
int main()
{std::cout << a << std::endl;
}
输出100;
(2)对于static类型的常量,在不同转换单元可以重复定义;对比inline类型
//test.cpp
static int a{ 100 };
void test()
{std::cout << "test.cpp中第一次打印:" << a << std::endl;a += 100;std::cout <<"test.cpp中第二次打印:" << a << std::endl;
}//main.cpp
static int a{ 100 };
void test();
int main()
{a += 100;test();std::cout <<"main.cpp中打印:" << a;
}
输出:
test.cpp中第一次打印:100
test.cpp中第二次打印:200
main.cpp中打印:200
反汇编:
main(void)a += 100;
00ED1DE6 mov eax,dword ptr [a (0EDD010h)]
00ED1DEB add eax,64h
00ED1DEE mov dword ptr [a (0EDD010h)],eax
test(void)a += 100;
00ED6428 mov eax,dword ptr [a (0EDD00Ch)]
00ED642D add eax,64h
00ED6430 mov dword ptr [a (0EDD00Ch)],eax
可以看到,static修饰的变量具有内部链接属性, 作用范围是在转换单元内
名称的链接属性
概念:
程序中的变量,函数,结构等都有自己的名字,这些名字具有不同的链接属性;
链接器根据这些链接属性 把各个对象文件 链接起来
链接属性:
内部链接属性:该名称仅仅在本转换单元中有效 const,static
外部链接属性:该名称在其他的转换单元中也有效 extern,inline
无链接属性:该名称仅仅能够用于该名称的作用域内访问
#define 用法
(0)配合#ifdef 来检测宏
#ifdef _H_
#define _H_
//...
#endif //_H_
(1)C语言中经常用#define来定义常量
例如:
#define MAX 1000;
而在C++中,更常用:
const int MAX{ 100 };
const这种写法的好处是限制了常量的类型,不容易用错;
(2)定义复杂表达式宏
例子:
//main.cpp
#define SUM(X,Y) X+Y
#define RELEASE(x) delete[] x;x=nullptr;std::cout<<#x<<":"<<x<<std::endl;
#define SHOWDIGIT(X) std::cout<<X<<std::endl;
#define SHOWSTRING(X) std::cout<<#X<<std::endl;
#define SHOW(X,Y,Z) void X##Y(){std::cout<<#Z<<std::endl;};
SHOW(test,_1,这是用宏定义建立的函数)int main()
{std::cout << SUM(1, 2) << std::endl;int* a = new int[10] {};RELEASE(a);SHOWDIGIT(123456);SHOWSTRING(abcdefg);test_1();
}
输出:
3
a:00000000
123456
abcdefg
这是用宏定义建立的函数
(3)#undef 取消宏定义
例如:
#define MAX 100;
#undef MAX;
预处理指令逻辑
#ifdef #ifndef #else #elif #endif
//可以用来做环境控制、版本控制等
#if支持表达式
#define VERSION 2
#if VERSION==1
int a = 1;
#elif VERSION==2
int a = 2;
#else
int a = 99;
#endif
#ifndef _H_
//#define _H_
//...
#else
//...
#endif //_H_
预定义宏
编译器已经定义好的宏
标准预定义标识符
_ _ func_ _
编译器支持ISO C99和ISO C++11指定的预定义标识符
作用:返回函数名称
int main()
{std::cout << __func__ << std::endl;
}
输出:
main
_ _ DATE_ _ , _ _ TIME_ _ , _ _ FILE_ _ , _ _ LINE_ _
分别是:
源文件的编译日期
当前转换单元的转换日期
源文件的名称
当前的行号
int main()
{std::cout << __DATE__ << std::endl;std::cout << __TIME__ << std::endl;std::cout << __FILE__ << std::endl;std::cout << __LINE__ << std::endl;
}
输出:
Jan 12 2025
20:34:59
I:\vsprogram\ConsoleApplication3\ConsoleApplication3\main.cpp
10
_ _ cplusplus
当翻译单元为C++时,_ _ cplusplus定义为1个整数文本,否则为未定义
int main()
{std::cout << __cplusplus << std::endl;
}
输出:
199711
MSVC的预定义宏
部分:
assert
语法:
assert(bool 表达式);
作用:
如果括号内的bool表达式为false
则会调用std::abort()函数
头文件
assert宏需要头文件 cassert
特点:
Debug才生效
Release会使语句失效
用途:
帮助我们调试,找错误
手动屏蔽:
#define NDEBUG
必须写在#include前面
示例:
#include <cassert>
int main()
{int a;std::cin >> a;assert(a);std::cout << a;
}
输入0时
屏蔽:
#define NDEBUG
#include <cassert>
int main()
{int a;std::cin >> a;assert(a);std::cout << a;
}
assert的定义:
#ifdef NDEBUG#define assert(expression) ((void)0)
#else_ACRTIMP void __cdecl _wassert(_In_z_ wchar_t const* _Message,_In_z_ wchar_t const* _File,_In_ unsigned _Line);#define assert(expression) (void)( \(!!(expression)) || \(_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \)
#endif
static_assert C++17的新语法
用于编译时检查重要的条件
用法示例:
int main()
{static_assert(sizeof(char*) == 4, "This isn't x86!!!!!!");
}
x64环境编译时: