《华为C&C++语言安全规范》笔记_华为 c&c++ 语言安全编程规范-程序员宅基地

技术标签: C\C++  c++  代码规范  c  安全  华为  

《华为C&C++语言安全规范》笔记

通过阅读《华为C&C++语言安全规范》1,我了解到了我在编程中很多缺失的部分。现在记录下几个要点:

规则1.1.4:严禁对指针变量进行sizeof操作

编码人员往往由于粗心,将指针当做数组进行sizeof操作,导致实际的执行结果与预期不符。 下面的代码,buffer和path分别是指针和数组,编码人员想对这2个内存进行清0操作,但由于编码人员的疏忽,第5行代码,将内存大小误写成了sizeof,与预期不符。
如果要判断当前的指针类型大小,请使用sizeof(char *)的方式。

char *buffer = (char *)malloc(size);
char path[MAX_PATH] = {
     0};
...
memset(path, 0, sizeof(path));
memset(buffer, 0, sizeof(buffer));  

相关指南:
CERT.ARR01-C. Do not apply the sizeof operator to a pointer when taking the size of an array

指针与数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。但是数组和指针存在差异。指针,是一个变量,存储的数据是地址。数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组,其外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量2。数组名只是大多数时候隐式转换成指向首元素的指针类型右值。这些时候不会转换:1)对其用 &;2)对其用 sizeof;3)C++中取引用3

我们先来一段代码作为演示:

#include<iostream>
using namespace std;
void func(int C[])
{
    
    cout<<"In function, C sizeof(C):"<<sizeof(C)<<endl;
    cout<<"C point:"<<C<<endl;
    cout<<"C &point:"<<&C<<endl;
    C++;
}
int main()
{
    
    int A[10];
    int* B=new int[10];
    cout<<"A sizeof(A):"<<sizeof(A)<<endl;
    cout<<"B sizeof(B):"<<sizeof(B)<<endl;
    // 取引用地址
    cout<<"A point:"<<A<<endl;
    cout<<"A &point:"<<&A<<endl;
    cout<<"B point:"<<B<<endl;
    cout<<"B &point:"<<&B<<endl;

    //调用函数
    func(A);
    //A++;//Error
    return 0;
}

在X86的编译环境下,输出的结果为:

A sizeof(A):40
B sizeof(B):4
A point:0x6dfec8
A &point:0x6dfec8
B point:0x1fa838
B &point:0x6dfec4
In function, C sizeof(C):4
C point:0x6dfec8
C &point:0x6dfeb0

显然第14行输出的是数组长度,第15行输出的是指针长度(在X86下为4字节,在x64环境下为8字节)。

第17-第20行,对数组取引用,其地址和本身的地址是一样,而指针则不一样。

第23行,当调用函数的时候,数组会转换为指针,因此长度为4,并且可以做自加运算。

规则1.2.1:断言必须使用宏定义,禁止直接调用系统提供的assert()

断言只能在调试版使用,断言被触发后,程序会立即退出,因此严禁在正式发布版本使用断言,请通过编译选项进行控制。 错误用法如:

int Foo(int *array, int size)
{
     
    assert(array != NULL);
    ...
}

ASSERT()是MFC的宏4,ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略。
assert()的功能类似,它是ANSI C标准中规定的函数,它与ASSERT的一个重要区别是可以用在Release版本中5

在msvc16里面是这样定义的:

#undef 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

在MinGW里面

/* According to C99 standard (section 7.2) the assert
   macro shall be redefined each time assert.h gets
   included depending on the status of NDEBUG macro.  */
#undef assert
...
#ifdef NDEBUG
#define assert(_Expression) ((void)0)
#else /* !defined (NDEBUG) */
#if defined(_UNICODE) || defined(UNICODE)
#define assert(_Expression) \
 (void) \
 ((!!(_Expression)) || \
  (_wassert(_CRT_WIDE(#_Expression),_CRT_WIDE(__FILE__),__LINE__),0))
#else /* not unicode */
#define assert(_Expression) \
 (void) \
 ((!!(_Expression)) || \
  (_assert(#_Expression,__FILE__,__LINE__),0))
#endif /* _UNICODE||UNICODE */
#endif /* !defined (NDEBUG) */

综上所述,使用语言自带的assert即可,也没有其他的选择。当在Release环境下,assert也自动编译为((void)0)对于华为的这条安全规范表示不解,希望有人能解答一下。

另外:这里还发现很有意思的东西:

/* According to C99 standard (section 7.2) the assert
macro shall be redefined each time assert.h gets
included depending on the status of NDEBUG macro. */

意思就是每次包含<assert.h>时,都会根据NDEBUG的当前状态重新定义assert宏6

规则1.2.3:严禁在断言内改变运行环境

在程序正式发布阶段,断言不会被编译进去,为了确保调试版和正式版的功能一致性,严禁在断言中使用任何赋值、修改变量、资源操作、内存申请等操作。 例如,以下的断言方式是错误的:

ASSERT(p1 = p2); //p1被修改
ASSERT(i++ > 1000); //i被修改
ASSERT(close(fd) == 0);//fd被关闭

建议1.2.1:不要将多条语句放在同一个断言中

为了更加准确地发现错误的位置,每一条断言只校验一个条件。 下面的断言同时校验多个条件,在断言触发的时
候,无法判断到底是哪一个条件导致的错误:

int Foo(int *array, int size)
{
     
	ASSERT(array != NULL && size > 0 && size < MAX_SIZE);
	...
}

应该将每个条件分开:

int Foo(int *array, int size)
{
     
    ASSERT(array != NULL);
    ASSERT(size > 0);
    ASSERT(size < MAX_SIZE);
    ...
}

规则1.3.2:严禁对公共接口API函数的参数进行ASSERT操作

对于设计成API的函数,必须对参数进行合法性判断,严禁在API实现过程中产生RASH。对API函数的参数进行ASSERT操作是没有意义的。 例如,对于提供应用服务器IP的平台公共API接口这样实现是错误的:

int GetServerIP(char *ip, size_t ipSize)
{
     
    ASSERT(ip != NULL);
    ...
}

公共接口API应当对输入参数进行代码检查:

int GetServerIP(char *ip, size_t ipSize)
{
     
    if (ip == NULL) {
     
    	...
    }
    ...
}

建议1.3.1:谨慎使用不可重入函数

不可重入函数在多线程环境下其执行结果不能达到预期效果,需谨慎使用。常见的不可重入函数包括:
rand, srand
getenv, getenv_s
strtok
strerror
asctime, ctime, localtime, gmtime
setlocale
atomic_init
tmpnam
mbrtoc16, c16rtomb, mbrtoc32, c32rtomb
gethostbyaddr
gethostbyname
inet_ntoa

在这里插入图片描述7

建议1.3.2:字符串或指针作为函数参数时,请检查参数是否为NULL

如果字符串或者指针作为函数参数,为了防止空指针引用错误,在引用前必须确保该参数不为NULL,如果上层调用者已经保证了该参数不可能为NULL,在调用本函数时,在函数开始处可以加ASSERT进行校验。 例如下面的代码,因为BYTE *p有可能为NULL,因此在使用前需要进行判断。

int Foo(int *p, int count)
{
     
    if (p != NULL && count > 0) {
     
        int c = p[0];
    }
    ...
}
int Foo2()
{
     
    int *arr = ...
    int count = ...
    Foo(arr, count);
    ...
}

下面的代码,由于p的合法性由调用者保证,对于Foo函数,不可能出现p为NULL的情况,因此加上ASSERT进行校验。

int Foo(int *p, int count)
{
     
    ASSERT(p != NULL); //ASSERT is added to verify p.
    ASSERT(count > 0);
    int c = p[0];
    	...
    }
int Foo2()
{
     
    int *arr = ...
    int count = ...
    ...
    if (arr != NULL && count > 0) {
     
    	Foo(arr, count);
    }
    ...
}

规则1.5.1:禁用C++异常机制

严禁使用C++的异常机制,所有的错误都应该通过错误值在函数之间传递并做相应的判断, 而不应该通过异常机制进行错误处理。 编码人员必须完全掌控整个编码过程,建立攻击者思维,增强安全编码意识,主动把握有可能出错的环节。而使用C++异常机制进行错误处理,会削弱编码人员的安全意识。 异常机制会打乱程序的正常执行流程,使程序结构更加复杂,原先申请的资源可能会得不到有效清理。 异常机制导致代码的复用性降低,使用了异常机制的代码,不能直接给不使用异常机制的代码复用。 异常机制在实现上依赖于编译器、操作系统、处理器,使用异常机制,导致程序执行性能降低。 在二进制层面,程序被加载后,异常处理函数增加了程序的被攻击面,攻击者可以通过覆盖异常处理函数地址,达到攻击的效果。 例外: 在接管C++语言本身抛出的异常(例如new失败、STL)、第三方库(例如IDL)抛出的异常时,可以使用异常机制,例如:

int len = ...;
char *p = NULL;
try {
     
	p = new char[len];
}
catch (bad_alloc) {
     
    ...
    abort();
}

相关指南:
Google C++ Style Guide.Exceptions: We do not use C++ exceptions.

这点在《游戏引擎架构8》中也提到过。

规则1.6.3:严禁在构造函数中创建线程

构造函数内仅作成员变量的初始化工作,其他的操作通过成员函数完成。

规则1.6.5:如果类的公共接口中返回类的私有数据地址,则必须加const类

实例:

class CMsg {
     
public:
    CMsg();
    ~CMsg();
    Const unsigned char *GetMsg();
protected:
    int size;
    unsigned char *msg;
};
CMsg::CMsg()
{
     
    size = 0;
    msg = NULL;
}
const unsigned char *CMsg::GetMsg()
{
     
	return msg;
}

规则1.7.3:禁用pthread_exit、ExitThread函数

严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。

建议1.7.1:禁用exit、ExitProcess函数(main函数除外)

程序应该安全退出,除了main函数以外,禁止任何地方调用exit、ExitProcess函数退出进程。直接退出进程会导致代码的复用性降低,资源得不到有效地清理。 程序应该通过错误值传递的机制进行错误处理。 以下代码加载文件,加载过程中如果出错,直接调用exit退出:

void LoadFile(const char *filePath)
{
     
    FILE* fp = fopen(filePath, "rt");
    if (fp == NULL) {
     
    	exit(0);
    }
    ...
}

正确的做法应该通过错误值传递机制,例如:

BOOL LoadFile(const char *filePath)
{
     
    BOOL ret = FALSE;
    FILE* fp = fopen(filePath, "rt");
    if (fp != NULL) {
     
    	...
    }
    ...
    return ret;
}

建议1.7.2:禁用abort函数

abort会导致程序立即退出,资源得不到清理。 例外: 只有发生致命错误,程序无法继续执行的时候,在错误处理函数中使用abort退出程序,例如:

void FatalError(int sig)
{
     
	abort();
}
int main(int argc, char *argv[])
{
     
       signal(SIGSEGV, FatalError);
       ...
}

规则2.5:调用格式化函数时,禁止format参数由外部可控

调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。 这些格式化函数有: 格式化输出函数:xxxprintf 格式化输入函数:xxxscanf 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line(); 格式化日志函数:syslog(),vsyslog()。
错误示例:

char *msg = GetMsg();
...
printf(msg);

推荐做法:

char *msg = GetMsg();
...
printf("%s\n", msg);

相关指南:
CERT.FIO47-C. Use valid format strings
MITRE.CWE-134: Use of Externally-Controlled Format String

规则4.2:整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值

由于整数在运算过程中可能溢出,当运算结果赋值给比他更大类型,或者和比他更大类型进行比较时,会导致实际结果与预期结果不符。 请观察以下二个代码及其输出:

int main(int argc, char *argv[])
{
     
    unsigned int a = 0x10000000;
    unsigned long long b = a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}

输出: b = B0000000

int main(int argc, char *argv[])
{
     
    unsigned int a = 0x10000000;
    unsigned long long b = (unsigned long long )a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}

输出: b = AB0000000

规则4.3:禁止对有符号整数进行位操作符运算

位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数。 错误示例:

int data = ReadByte();
int a = data >> 24;

推荐做法:(为简化示例代码,此处假设ReadByte函数实际不存在返回值小于0的情况)

unsigned int data = (unsigned int)ReadByte();
unsigned int a = data >> 24;

相关指南:
CERT.INT13-C. Use bitwise operators only on unsigned operands
MISRA.C.2004.Rule 12.7 (required): Bitwise operators shall not be applied to operands whose underlyingtype is signed.

在C语言中,如果在未知的有符号上执行位操作,很可能会导致缓冲区溢出,从而在某些情况下导致攻击者执行任意代码,同时,还可能会出现出乎意料的行为或编译器定义的行为。

代码如下:

#include<stdio.h>
int main()
{
    
    int y=0x80000000;
	printf("%d\n",y>>24);//以十进制有符号形式输出。
	printf("%u\n",y>>24);//以十进制无符号形式输出。
    return 0;
}

输出为:

-128
4294967168

由于int类型的最高位是符号位,剩下的31位才用来存储数值,y>>24的结果应该是0xffffff80(负数右移,左补1),当我们以无符号整型输出时,为正数的0xffffff80,以有符号整型输出时,应减一再取反,结果为-1289

然而,在右移运算中,空出的位用 0 还是符号位进行填充是由于具体的 C 语言编译器实现来决定。在通常情况下,如果要进行移位的操作数是无符号类型的,那么空出的位将用 0 进行填充;如果要进行移位的操作数是有符号类型的,则 C 语言编译器实现既可选择 0 来进行填充,也可选择符号位进行填充。

因此,如果很关心一个右移运算中的空位,那么可以使用 unsigned 修饰符来声明变量,这样空位都会被设置为 0。同时,如果一个程序采用了有符号数的右移位操作,那么它就是不可移植的10

规则4.4:禁止整数与指针间的互相转化

指针的大小随着平台的不同而不同,强行进行整数与指针间的互相转化,降低了程序的兼容性,在转换过程中可能引起指针高位信息的丢失。
错误示例:

char *ptr = ...;
unsigned int number = (unsigned int)ptr;

推荐做法:

char *ptr = ...;
uintptr_t number = (uintptr_t)ptr;

相关指南:
CERT.INT36-C. Converting a pointer to integer or integer to pointer
MISRA.C.2004.Rule 11.3 (advisory): A cast should not be performed between a pointer type and an integral type.

规则4.5:禁止对指针进行逻辑或位运算(&&、||、!、~、>>、<<、&、^、|)

对指针进行逻辑运算,会导致指针的性质改变,可能产生内存非法访问的问题。 下面是错误的用法:

BOOL dealName(const char *nameA, const char *nameB)
{
     
    ...
    if (nameA)
    ...
    if (!nameB)
    ...
}

下面是正确的用法:

BOOL dealName(const char *nameA, const char *nameB)
{
     
    ...
    if (nameA != NULL)
    ...
    if (nameB == NULL)
    ...
}

例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
相关指南:
MISRA.C.2004.Rule 12.6 (advisory): The operands of logical operators (&&, || and !) should be effectively Boolean. Expressions that are effectively Boolean should not be used as operands to operators other than (&&, ||, !, =, ==, != and ?.

规则5.1:内存申请前,必须对申请内存大小进行合法性校验

内存申请的大小可能来自于外部数据,必须检查其合法性,防止过多地、非法地申请内存。不能申请0长度的内存。 例如:

int Foo(int size)
{
     
    if (size <= 0) {
     
    //error
    ...
    }
    ...
    char *msg = (char *)malloc(size);
    ...
}

相关指南:
CERT.MEM04-C. Beware of zero-length allocations
MITRE.CWE-789: Uncontrolled Memory Allocation

规则5.2:内存分配后必须判断是否成功

char *msg = (char *)malloc(size);
if (msg != NULL) {
     
	...
}

相关指南:
CERT.MEM11-C. Do not assume infinite heap space
CERT.ERR33-C. Detect and handle standard library errors
CERT.MEM52-CPP. Detect and handle memory allocation errors
MITRE.CWE 252, Unchecked Return Value
MITRE.CWE 391, Unchecked Error Condition
MITRE.CWE 476, NULL Pointer Dereference
MITRE.CWE 690, Unchecked Return Value to NULL Pointer Dereference
MITRE.CWE 703, Improper Check or Handling of Exceptional Conditions
MITRE.CWE 754, Improper Check for Unusual or Exceptional Conditions

规则5.3:禁止引用未初始化的内存

malloc、new分配出来的内存没有被初始化为0,要确保内存被引用前是被初始化的。 以下代码使用malloc申请内存,在使用前没有初始化:

int *CalcMetrixColomn( int **metrix ,int *param, size_t size )
{
     
    int *result = NULL;
    ...
    size_t bufSize = size * sizeof(int);
    ...
    result = (int *)malloc(bufSize);
    ...
    result[0] += metrix[0][0] * param[0];
    ...
    return result;
}

以下代码使用memset_s()对分配出来的内存清零。

int *CalcMetrixColomn(int **metrix ,int *param, size_t size)
{
     
    int *result = NULL;
    ...
    size_t bufSize = size * sizeof(int);
    ...
    result = (int *)malloc(bufSize);
    ...
    int ret = memset_s(result, bufSize, 0, bufSize); //【修改】确保内存被初始化后才被引用
    ...
    result[0] += metrix[0][0] * param[0];
    ...
    return result;
}

相关指南:
CERT.EXP33-C. Do not read uninitialized memory
CERT.EXP53-CPP. Do not read uninitialized memory

规则5.4:内存释放之后立即赋予新值

悬挂指针可能会导致双重释放(double-free)以及访问已释放内存的危险。消除悬挂指针以及消除众多与内存相关危险的一个最为有效地方法就是当指针使用完后将其置新值。 如果一个指针释放后能够马上离开作用域,因为它已经不能被再次访问,因此可以无需对其赋予新值。
示例:

char *message = NULL;
    ...
    message = (char *)malloc(len);
    ...
    if (...) {
     
        free(message); //在这个分支内对内存进行了释放
        message = NULL; //释放后将指针赋值为NULL
    }
    ...
    if (message != NULL) {
     
        free(message);
        message = NULL;
}

相关指南:
CERT.MEM01-C. Store a new value in pointers immediately after free()
CERT.MEM50-CPP. Do not access freed memory

规则5.5:禁止使用realloc()函数

realloc()原型如下:

void *realloc(void *ptr, size_t size);

随着参数的不同,其行为也是不同。 1) 当ptr不为NULL,且size不为0时,该函数会重新调整内存大小,并将新的内存指针返回,并保证最小的size的内容不变; 2) 参数ptr为NULL,但size不为0,那么行为等同于malloc(size); 3) 参数size为0,则realloc的行为等同于free(ptr)。 由此可见,一个简单的C函数,却被赋予了3种行为,这不是一个设计良好的函数。虽然在编码中提供了一些便利性,但是却极易引发各种bug。
相关指南:
CERT.MEM36-C. Do not modify the alignment of objects by calling realloc()

规则5.6:禁止使用alloca()函数申请栈上内存

POSIX和C99均未定义alloca()的行为,在有些平台下不支持该函数,使用alloca会降低程序的兼容性和可移植性,该函数在栈帧里申请内存,申请的大小很可能超过栈的边界,影响后续的代码执行。 请使用malloc或new,从堆中动态分配内存。

规则7.1:创建文件时必须显式指定合适的文件访问权限

创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件。 下列代码没有显式配置文件的访问权限。

int fd = open(fileName, O_CREAT | O_WRONLY); //【错误】缺少访问权限设置

推荐做法:

int fd = open(fileName, O_CREAT | O_WRONLY, S_IRUSR|S_IWUSR);

规则8.3:严禁使用string类存储敏感信息

string类是C++内部定义的字符串管理类,如果口令等敏感信息通过string进行操作,在程序运行过程中,敏感信息可能会散落到内存的各个地方,并且无法清0。 以下代码,Foo函数中获取密码,保存到string变量password中,随后传递给VerifyPassword函数,在这个过程中,password实际上在内存中出现了2份。

int VerifyPassword(string password)
{
     
    //...
}
int Foo()
{
     
    string password = GetPassword();
    VerifyPassword(password);
    ...
}

应该使用char或unsigned char保存敏感信息,如下代码:

int VerifyPassword(const char *password)
{
     
    //...
}
int Foo()
{
     
    char password[MAX_PASSWORD] = {
     0};
    GetPassword(password, sizeof(password));
    VerifyPassword(password);
    ...
}

  1. 《华为C&C++语言安全规范》 https://www.jb51.net/books/720904.html

  2. C/C++数组名与指针区别深入探索_ljob2006的博客-程序员宅基地 https://blog.csdn.net/ljob2006/article/details/4872167

  3. c中,数组名跟指针有区别吗? - 知乎 https://www.zhihu.com/question/41805285

  4. 大小写 ASSERT 有什么区别_百度知道 https://zhidao.baidu.com/question/110720542.html

  5. 问题: 什么是ASSERT()? ASSERT()和assert()的区别是什么?_流风的专栏-程序员宅基地 https://blog.csdn.net/procedurecode/article/details/1942115

  6. 第一章assert.h_暮秋小屋-程序员宅基地 https://blog.csdn.net/qq_49150256/article/details/112753240

  7. 可重入函数对于线程安全的意义(附函数表) - 云+社区 - 腾讯云 https://cloud.tencent.com/developer/article/1685935

  8. 游戏引擎架构 (豆瓣) https://book.douban.com/subject/25815142/

  9. 有符号执行位操作导致的BUG_Tonson_的博客-程序员宅基地 https://blog.csdn.net/weixin_44444450/article/details/109668780

  10. 位操作及其使用注意事项,C语言位操作及其使用方法详解 http://c.biancheng.net/view/362.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/z736248591/article/details/117767444

智能推荐

Docker 快速上手学习入门教程_docker菜鸟教程-程序员宅基地

文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程

电脑技巧:Windows系统原版纯净软件必备的两个网站_msdn我告诉你-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你

vue2封装对话框el-dialog组件_<el-dialog 封装成组件 vue2-程序员宅基地

文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_

MFC 文本框换行_c++ mfc同一框内输入二行怎么换行-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行

redis-desktop-manager无法连接redis-server的解决方法_redis-server doesn't support auth command or ismis-程序员宅基地

文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try

实验四 数据选择器及其应用-程序员宅基地

文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用

随便推点

灰色预测模型matlab_MATLAB实战|基于灰色预测河南省社会消费品零售总额预测-程序员宅基地

文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件

log4qt-程序员宅基地

文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt

100种思维模型之全局观思维模型-67_计算机中对于全局观的-程序员宅基地

文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的

线程间控制之CountDownLatch和CyclicBarrier使用介绍_countdownluach于cyclicbarrier的用法-程序员宅基地

文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法

自动化监控系统Prometheus&Grafana_-自动化监控系统prometheus&grafana实战-程序员宅基地

文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战

React 组件封装之 Search 搜索_react search-程序员宅基地

文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search