在C语言中,是否有一种干净、最好是标准的方法可以从字符串中裁剪前导和尾随的空格?我可以自己编写代码,但我认为这是一个常见的问题,应该有同样常见的解决方案。
在C语言中,是否有一种干净、最好是标准的方法可以从字符串中裁剪前导和尾随的空格?我可以自己编写代码,但我认为这是一个常见的问题,应该有同样常见的解决方案。
如果您可以修改字符串:
// Note: This function returns a pointer to a substring of the original string.
// If the given string was allocated dynamically, the caller must not overwrite
// that pointer with the returned value, since the original pointer must be
// deallocated using the same allocator with which it was allocated. The return
// value must NOT be deallocated using free() etc.
char *trimwhitespace(char *str)
{
char *end;
// Trim leading space
while(isspace((unsigned char)*str)) str++;
if(*str == 0) // All spaces?
return str;
// Trim trailing space
end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
// Write new null terminator character
end[1] = '\0';
return str;
}
如果您无法修改字符串,则可以使用基本相同的方法:
// Stores the trimmed input string into the given output buffer, which must be
// large enough to store the result. If it is too small, the output is
// truncated.
size_t trimwhitespace(char *out, size_t len, const char *str)
{
if(len == 0)
return 0;
const char *end;
size_t out_size;
// Trim leading space
while(isspace((unsigned char)*str)) str++;
if(*str == 0) // All spaces?
{
*out = 0;
return 1;
}
// Trim trailing space
end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
end++;
// Set output size to minimum of trimmed string length and buffer size minus 1
out_size = (end - str) < len-1 ? (end - str) : len-1;
// Copy trimmed string and add null terminator
memcpy(out, str, out_size);
out[out_size] = 0;
return out_size;
}
这里有一个将字符串移动到缓冲区第一位置的方法。您可能希望采用这种行为,这样,如果您动态分配了该字符串,仍然可以在与trim()返回的指针相同的指针上释放它:
char *trim(char *str)
{
size_t len = 0;
char *frontp = str;
char *endp = NULL;
if( str == NULL ) { return NULL; }
if( str[0] == '\0' ) { return str; }
len = strlen(str);
endp = str + len;
/* Move the front and back pointers to address the first non-whitespace
* characters from each end.
*/
while( isspace((unsigned char) *frontp) ) { ++frontp; }
if( endp != frontp )
{
while( isspace((unsigned char) *(--endp)) && endp != frontp ) {}
}
if( frontp != str && endp == frontp )
*str = '\0';
else if( str + len - 1 != endp )
*(endp + 1) = '\0';
/* Shift the string so that it starts at str so that if it's dynamically
* allocated, we can still free it on the returned pointer. Note the reuse
* of endp to mean the front of the string buffer now.
*/
endp = str;
if( frontp != str )
{
while( *frontp ) { *endp++ = *frontp++; }
*endp = '\0';
}
return str;
}
测试正确性:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
/* Paste function from above here. */
int main()
{
/* The test prints the following:
[nothing to trim] -> [nothing to trim]
[ trim the front] -> [trim the front]
[trim the back ] -> [trim the back]
[ trim front and back ] -> [trim front and back]
[ trim one char front and back ] -> [trim one char front and back]
[ trim one char front] -> [trim one char front]
[trim one char back ] -> [trim one char back]
[ ] -> []
[ ] -> []
[a] -> [a]
[] -> []
*/
char *sample_strings[] =
{
"nothing to trim",
" trim the front",
"trim the back ",
" trim front and back ",
" trim one char front and back ",
" trim one char front",
"trim one char back ",
" ",
" ",
"a",
"",
NULL
};
char test_buffer[64];
char comparison_buffer[64];
size_t index, compare_pos;
for( index = 0; sample_strings[index] != NULL; ++index )
{
// Fill buffer with known value to verify we do not write past the end of the string.
memset( test_buffer, 0xCC, sizeof(test_buffer) );
strcpy( test_buffer, sample_strings[index] );
memcpy( comparison_buffer, test_buffer, sizeof(comparison_buffer));
printf("[%s] -> [%s]\n", sample_strings[index],
trim(test_buffer));
for( compare_pos = strlen(comparison_buffer);
compare_pos < sizeof(comparison_buffer);
++compare_pos )
{
if( test_buffer[compare_pos] != comparison_buffer[compare_pos] )
{
printf("Unexpected change to buffer @ index %u: %02x (expected %02x)\n",
compare_pos, (unsigned char) test_buffer[compare_pos], (unsigned char) comparison_buffer[compare_pos]);
}
}
}
return 0;
}
源文件名为trim.c。使用'cc -Wall trim.c -o trim'进行编译。
isspace
的参数转换为 unsigned char
,否则会引发未定义的行为。 - Roland Illigisspace()
,那么" "
和"\n"
之间有什么区别呢?我已经添加了针对换行符的单元测试,看起来没问题...https://ideone.com/bbVmqo - indiv*(endp + 1) = '\0';
。答案中的示例测试使用了一个大小为64的缓冲区,从而避免了这个问题。 - Simas char* ptr = strdup(" ");
printf("trim is [%s]\n", trim(ptr));
free(ptr);
return 0;
}```
`gcc -g test.c; valgrind a.out`
这是因为当frontp移动到字符串的末尾时,endp从未向后移动超出缓冲区的最终空终止符,因此写入*(endp+1)超出了缓冲区。修复方法是交换围绕第22行的“if”和“else if”操作的顺序。
- nolandda我的解决方案。字符串必须是可变的。相较于其他一些解决方案,它的优点在于将非空格部分移动到开头,因此您可以继续使用旧指针,以防以后需要释放它。
void trim(char * s) {
char * p = s;
int l = strlen(p);
while(isspace(p[l - 1])) p[--l] = 0;
while(* p && isspace(* p)) ++p, --l;
memmove(s, p, l + 1);
}
此版本使用strndup()创建字符串副本,而不是直接进行原地编辑。strndup()需要_GNU_SOURCE,因此您可能需要使用malloc()和strncpy()创建自己的strndup()。
char * trim(char * s) {
int l = strlen(s);
while(isspace(s[l - 1])) --l;
while(* s && isspace(* s)) ++s, --l;
return strndup(s, l);
}
s
为空字符串,则调用trim()
会引起未定义行为,因为第一个isspace()
调用将会是isspace(p[-1])
,而p[-1]
不一定指向一个合法的位置。 - chux - Reinstate Monicaisspace
的参数转换为 unsigned char
,否则会引发未定义的行为。 - Roland Illigif(l==0)return;
来避免零长度的字符串。 - ch271828n这是我的C小型库,可对左侧、右侧、两侧、全部字符进行原地和分离的修剪,并修剪一组指定的字符(默认为空格)。
#ifndef STRLIB_H_
#define STRLIB_H_ 1
enum strtrim_mode_t {
STRLIB_MODE_ALL = 0,
STRLIB_MODE_RIGHT = 0x01,
STRLIB_MODE_LEFT = 0x02,
STRLIB_MODE_BOTH = 0x03
};
char *strcpytrim(char *d, // destination
char *s, // source
int mode,
char *delim
);
char *strtriml(char *d, char *s);
char *strtrimr(char *d, char *s);
char *strtrim(char *d, char *s);
char *strkill(char *d, char *s);
char *triml(char *s);
char *trimr(char *s);
char *trim(char *s);
char *kill(char *s);
#endif
#include <strlib.h>
char *strcpytrim(char *d, // destination
char *s, // source
int mode,
char *delim
) {
char *o = d; // save orig
char *e = 0; // end space ptr.
char dtab[256] = {0};
if (!s || !d) return 0;
if (!delim) delim = " \t\n\f";
while (*delim)
dtab[*delim++] = 1;
while ( (*d = *s++) != 0 ) {
if (!dtab[0xFF & (unsigned int)*d]) { // Not a match char
e = 0; // Reset end pointer
} else {
if (!e) e = d; // Found first match.
if ( mode == STRLIB_MODE_ALL || ((mode != STRLIB_MODE_RIGHT) && (d == o)) )
continue;
}
d++;
}
if (mode != STRLIB_MODE_LEFT && e) { // for everything but trim_left, delete trailing matches.
*e = 0;
}
return o;
}
// perhaps these could be inlined in strlib.h
char *strtriml(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_LEFT, 0); }
char *strtrimr(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_RIGHT, 0); }
char *strtrim(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_BOTH, 0); }
char *strkill(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_ALL, 0); }
char *triml(char *s) { return strcpytrim(s, s, STRLIB_MODE_LEFT, 0); }
char *trimr(char *s) { return strcpytrim(s, s, STRLIB_MODE_RIGHT, 0); }
char *trim(char *s) { return strcpytrim(s, s, STRLIB_MODE_BOTH, 0); }
char *kill(char *s) { return strcpytrim(s, s, STRLIB_MODE_ALL, 0); }
这个主要程序可以完成所有任务。如果src == dst,它会原地修剪,否则,它的工作方式类似于strcpy
例程。它可以修剪在字符串delim中指定的一组字符,或者如果为空,则修剪空格。它可以修剪左侧、右侧、两侧和所有(如tr)。它没有太多的内容,只需要对字符串进行一次迭代。有些人可能会抱怨trim right从左侧开始,但是,无论如何都需要从左侧开始计算strlen。 (为了进行正确的修剪,您必须以某种方式到达字符串的末尾,因此最好在进行迭代时进行计算。)可能会有关于流水线和缓存大小等方面的争论 - 谁知道呢。由于该解决方案从左到右工作并且仅迭代一次,因此可以扩展为在流上工作。限制:它不能处理unicode字符串。
dtab[*d]
作为数组下标之前,没有将 *d
强制转换为 unsigned int
。在一个带有有符号字符的系统上,这将读取到 dtab[-127]
,这可能会导致错误和崩溃。 - Zan Lynxdtab [* delim ++]
可能存在未定义行为,因为 char
索引值必须转换为 unsigned char
。 该代码假定 char
是 8 位的。 delim
应声明为 const char *
。 dtab [0xFF &(unsigned int)* d]
更清晰,因为它是 dtab [(unsigned char)* d]
。 该代码适用于 UTF-8 编码的字符串,但不会剥离非 ASCII 空格序列。 - chqrlie这是我尝试编写的一个简单但正确的原地修剪函数。
void trim(char *str)
{
int i;
int begin = 0;
int end = strlen(str) - 1;
while (isspace((unsigned char) str[begin]))
begin++;
while ((end >= begin) && isspace((unsigned char) str[end]))
end--;
// Shift all characters back to the start of the string array.
for (i = begin; i <= end; i++)
str[i - begin] = str[i];
str[i - begin] = '\0'; // Null terminate string.
}
while ((end >= begin) && isspace(str[end]))
,以防止在 str
为空字符串时出现 UB。这可以避免 str[-1]
的情况。 - chux - Reinstate Monicaisspace
的参数强制转换为 unsigned char
,否则会调用未定义的行为。 - Roland Illig<ctype.h>
中的函数旨在使用 int 类型,该类型表示无符号字符或特殊值 EOF。请参见 https://dev59.com/mVrUa4cB1Zd3GeqPkHUT。 - Roland Illigisspace()
的实际用途是测试字符是否为空格。 我想不到调用isspace()
来传入任意的int
值的原因。 所以我希望实现会处理char
,如果必要,就在内部执行一个无符号的char
转换。 但是你对标准的理解是正确的。 有时候我忘记了C标准是多么地保守和过时:( - wovano晚来的修剪派对
特点:
1. 快速修剪开头,与其他答案中的方法相似。
2. 到达结尾后,每个循环仅使用 1 次测试以右侧修剪。类似于 @jfm3,但适用于所有空格字符串。
3. 为避免符号化字符是有符号字符时出现未定义行为,请将 *s
强制转换为 unsigned char
。
字符处理“在所有情况下,该参数都是一个 int 值,其值应表示为无符号字符或等于宏 EOF 的值。如果参数具有任何其他值,则行为是未定义的。” C11 §7.4 1
#include <ctype.h>
// Return a pointer to the trimmed string
char *string_trim_inplace(char *s) {
while (isspace((unsigned char) *s)) s++;
if (*s) {
char *p = s;
while (*p) p++;
while (isspace((unsigned char) *(--p)));
p[1] = '\0';
}
// If desired, shift the trimmed string
return s;
}
@chqrlie评论说上面的代码没有移动截取后的字符串。要做到这一点,可以进行如下操作...
// Return a pointer to the (shifted) trimmed string
char *string_trim_inplace(char *s) {
char *original = s;
size_t len = 0;
while (isspace((unsigned char) *s)) {
s++;
}
if (*s) {
char *p = s;
while (*p) p++;
while (isspace((unsigned char) *(--p)));
p[1] = '\0';
// len = (size_t) (p - s); // older errant code
len = (size_t) (p - s + 1); // Thanks to @theriver
}
return (s == original) ? s : memmove(original, s, len + 1);
}
#include <ctype.h>
void trim(char * const a)
{
char *p = a, *q = a;
while (isspace(*q)) ++q;
while (*q) *p++ = *q++;
*p = '\0';
while (p > a && isspace(*--p)) *p = '\0';
}
/* See http://fctx.wildbearsoftware.com/ */
#include "fct.h"
FCT_BGN()
{
FCT_QTEST_BGN(trim)
{
{ char s[] = ""; trim(s); fct_chk_eq_str("", s); } // Trivial
{ char s[] = " "; trim(s); fct_chk_eq_str("", s); } // Trivial
{ char s[] = "\t"; trim(s); fct_chk_eq_str("", s); } // Trivial
{ char s[] = "a"; trim(s); fct_chk_eq_str("a", s); } // NOP
{ char s[] = "abc"; trim(s); fct_chk_eq_str("abc", s); } // NOP
{ char s[] = " a"; trim(s); fct_chk_eq_str("a", s); } // Leading
{ char s[] = " a c"; trim(s); fct_chk_eq_str("a c", s); } // Leading
{ char s[] = "a "; trim(s); fct_chk_eq_str("a", s); } // Trailing
{ char s[] = "a c "; trim(s); fct_chk_eq_str("a c", s); } // Trailing
{ char s[] = " a "; trim(s); fct_chk_eq_str("a", s); } // Both
{ char s[] = " a c "; trim(s); fct_chk_eq_str("a c", s); } // Both
// Villemoes pointed out an edge case that corrupted memory. Thank you.
// https://dev59.com/knVD5IYBdhLWcg3wAGiD#NZ0IoYgBc1ULPQZFJ5LK
{
char s[] = "a "; // Buffer with whitespace before s + 2
trim(s + 2); // Trim " " containing only whitespace
fct_chk_eq_str("", s + 2); // Ensure correct result from the trim
fct_chk_eq_str("a ", s); // Ensure preceding buffer not mutated
}
// doukremt suggested I investigate this test case but
// did not indicate the specific behavior that was objectionable.
// http://stackoverflow.com/posts/comments/33571430
{
char s[] = " foobar"; // Shifted across whitespace
trim(s); // Trim
fct_chk_eq_str("foobar", s); // Leading string is correct
// Here is what the algorithm produces:
char r[16] = { 'f', 'o', 'o', 'b', 'a', 'r', '\0', ' ',
' ', 'f', 'o', 'o', 'b', 'a', 'r', '\0'};
fct_chk_eq_int(0, memcmp(s, r, sizeof(s)));
}
}
FCT_QTEST_END();
}
FCT_END();
另一个例子,只需要一行代码就能完成真正的工作:
#include <stdio.h>
int main()
{
const char *target = " haha ";
char buf[256];
sscanf(target, "%s", buf); // Trimming on both sides occurs here
printf("<%s>\n", buf);
}
%n
转换说明符计算跳过的字符数,在最后手动处理可能更简单。 - Peter - Reinstate Monicavoid fnStrTrimInPlace(char *szWrite) {
const char *szWriteOrig = szWrite;
char *szLastSpace = szWrite, *szRead = szWrite;
int bNotSpace;
// SHIFT STRING, STARTING AT FIRST NON-SPACE CHAR, LEFTMOST
while( *szRead != '\0' ) {
bNotSpace = !isspace((unsigned char)(*szRead));
if( (szWrite != szWriteOrig) || bNotSpace ) {
*szWrite = *szRead;
szWrite++;
// TRACK POINTER TO LAST NON-SPACE
if( bNotSpace )
szLastSpace = szWrite;
}
szRead++;
}
// TERMINATE AFTER LAST NON-SPACE (OR BEGINNING IF THERE WAS NO NON-SPACE)
*szLastSpace = '\0';
}
isspace
的参数转换为 unsigned char
,否则会引发未定义的行为。 - Roland Illigwhile (isspace((unsigned char) *szWrite)) szWrite++;
可以防止这种情况发生。代码还会复制所有尾随的空格。 - chux - Reinstate Monica*szWrite = *szRead
才会跳过这种情况下的写入,但是我们又添加了另一个比较/分支。对于现代CPU/MMU/BP,我不知道这个检查是否会损失或获得性能。对于更简单的处理器和内存架构,直接进行复制并跳过比较更便宜。 - Jason Stewart
str
是一个局部变量,改变它并不会改变传入的原始指针。在C语言中,函数调用始终是按值传递,而不是按引用传递。 - Adam Rosenfieldfree()
函数的有效参数。相反,我设计它是为了避免出于效率考虑而需要进行内存分配。如果传入的地址是动态分配的,则调用者仍然负责释放该内存,并且调用者需要确保不要用此处返回的值覆盖那个值。 - Adam Rosenfieldisspace
函数的参数转换为unsigned char
类型,否则会导致未定义行为。 - Roland Illig