阅读提示:文章比较冗长,若为了快速做题,请用常规方法sizeof和方法六计数器即可,其他对完成作业都没什么特别的意义,最后感谢您的点击与阅读~
文章目录

数组与字符串
在C语言中,最精彩的地方果然还是属于数组与指针。所以写了一下在C/C++中,有关于求取数组与字符串长度的几种主流的方法。写得比较简略,若有疑问可以评论出来,若有不足欢迎指出,谢谢。
1 数组
静态数组:TYPE Array[NUM];
而动态数组:TYPE *Array = (TYPE *) malloc ( sizeof(TYPE) * NUM);
1.1 静态数组
对于任何数组而言,若长度已知,那么再写个求长度的函数是没有意义的,除非求有效长度,而非分配长度。比如,int array[N]
这种形式。
而且,由于静态数组通常在预编译的时候就知道了数组长度,所以通常没必要在程序运行时再计算一遍长度,如int array[]
。
当然,这种知道是机器知道,而不是人知道。如果人希望知道这个数组的长度大小,那么这时使用在程序预编译阶段求值的运算符、模板等东西就特别合适,尽管它们会有些缺陷。
(1)sizeof 运算符
sizeof运算符可以返回一个变量或者类型所占的内存字节数。那么使用sizeof,分别获取数组内存和单个元素内存,其值之比就是数组长度。
- sizeof - 常规方法
int array[] = {1,2,3,4,5,6,7,8,9}
int elementCount = sizeof (array) / sizeof (array[0]);
以下这几种写法是等价的:
1、整个数组所占字节数:sizeof(array) = sizeof(int[9])
2、单个元素所占字节数:sizeof(int) = sizeof(array[0]) = sizeof(array[9]) = sizeof(*array)
3、数组长度 = 整个数组所占字节数 / 单个元素所占字节数
不过,我们在C语言中习惯上在使用时都把它定义成一个宏:
- sizeof - 宏定义表达式
/*方法一:在C语言中,sizeof的宏定义*/
#include<stdio.h>
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int main(void)
{
int array[] = {1,2,3};
printf("\nThe length of the array is %lu .\n", ARRAY_SIZE(array));
return 0;
}
- sizeof - 宏定义函数体
/*方法二:在C语言中,sizeof宏定义的另外一种写法*/
#include<stdio.h>
#define GET_ARRAY_LEN(arr, len) {len = (sizeof(arr) / sizeof(arr[0]));}
int main(void)
{
int length = 0;
int array[] = {1,2,3};
GET_ARRAY_LEN(array, length);
printf("\nThe length of the array is %d .\n", length);
return 0;
}
- sizeof - Linux内核源码
由于上面的宏并不完整,缺少了传参的类型判断容易被误用,比如用在指针而不是数组上。那么,在Linux内核源码分析里,增加了一个判断是否为数组的操作。
//以下方法均可运行,欢迎尝试
/*方法一的延伸:在Kennel中,ARRAY_SIZE宏的标准写法,增加了变量类型判断*/
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
/*除此之外,还有另一种标准写法*/
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) \
+ sizeof(typeof(int[1 - 2*!!__builtin_types_compatible_p(typeof(arr), typeof(&arr[0]))]))*0)
/*当然,如果上述方法太长,可以利用typeof来判断类型*/
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof( ((typeof(arr)){})[0]))
关于sizeof的缺点:
1、不能封装成函数
其实这里不是sizeof的问题,而是数组在作为函数参数传递时,会退化为指针导致实参的信息丢失了,从而计算出错。函数只是传递一个数组首字节的地址,并没有传递其大小信息。原因很简单,C语言的参数都是值传递,如果数组很大会浪费许多不必要的空间,所以将数组转换成指针才是一个不错的选择。
所以如果是char型数组,可以利用‘\0’来作为边界计算。但对于其他数组,解决方法有二,要么选择引用数组与模板保存实参信息,要么传参时直接带上数组长度信息。
/*错误示范!*/
int ARRAY_SIZE(int arr[])//此处数组退化为指针
{
return sizeof(arr) / sizeof((arr)[0]);//爆出警示信息
}
2、无法求取有效长度
如果在定义数组时就给定了数组的大小。这样不管数组是增加还是减少元素,sizeof(a)/sizeof(a[0]) 都能自动求出数组的长度。需要注意的是,它求出的是数组的总长度,而不是数组中存放有意义数据的个数。
3、不能求取动态数组
首先,sizeof是运算符,不是函数。其次,sizeof不需要引用任何头文件就能够运行,它可以在程序预编译阶段就计算出值。所以,像sizeof这种在编译器间静态求值的,是不能在程序运行时求出动态改变内存的数组长度。
(2)template 模版
当数组退化为指针时,sizeof不能正确计算出结果,即使选择数组引用(&array)来防止其变为指针,但这也是不行的。这时应该选择数组引用+模板,这是因为模板会进行类型检查,能识别出来传参出错。
- template - 模版函数
/*方法三:在C++中,使用模板技术定义一个sizeof的函数*/
#include <iostream>
//using namespace std;
template <class T>
int getArrayLen(T& array) {
return (sizeof(array) / sizeof(array[0]));
}
int main(int argc, const char * argv[]) {
int array[] = {1,2,3};
std::cout << getArrayLen(array) << std::endl;
return 0;
}
- template - 函数模版
同样采用数组引用的方法,实现参数的自动推断,可以区分数组和指针。函数模版能在编译期获得数组的大小,并且如果参数是指针则会在编译期间报错。
/*方法四:在C++中,使用函数模版的参数自动推断*/
#include <iostream>
template <typename T, size_t N>
inline size_t countof( T (&array)[N] ) {
return N;
}
int main(int argc, const char * argv[]) {
int array[] = {1,2,3};
std::cout << countof(array) << std::endl;
return 0;
}
- template - 预定义宏
在<stdlib.h>头文件里的_countof宏,则结合了以上两种模版方法,利用了C++特性进行编译推导,具体讲解。
// From stdlib.h,运行失败,还在找原因
#if !defined(_countof)
#if !defined(__cplusplus)
#define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))
#else
extern "C++"
{
template <typename _CountofType, size_t _SizeOfArray>
char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
#define _countof(_Array) sizeof(*__countof_helper(_Array))
}
#endif
#endif
// From Google Chrome,运行成功
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
(3)std::begin 和 std::end
为了统一代码并让数组支持迭代器,C++11提供了两个新模版函数std::begin、std::end,这两个模版函数可以作用于容器和数组。
其中,begin()返回指向数组首元素的指针,end()返回指向尾元素的下一位置的指针,相减就能求出数组长度。
- C++迭代器库模板
/*方法五:在C++中,使用STL库中的迭代器模板函数*/
#include <iostream>
int main(int argc, const int * argv[]) {
int arr[] = {'1','2','3'};
std::cout << std::end(arr) - std::begin(arr) << std::endl;
return 0;
}
以上所有方法,均是在静态编译期间利用内存大小计算出长度,所以若遇到含’\0’的字符数组时,记得需要 length-1。
1.2 动态数组
对于动态数组长度,其一般是拿指针变量去遍历,主要使用计数器方法,采用累加得出数组长度。换而言之,动态数组一般由循环迭代生成,那么长度也由循环去迭代计算。
不好意思还没写代码, p = (int*)malloc(sizeof(int) * 10);
2 字符串
字符型数组:char str[ ] ;
字节型数组:byte str[ ] ; (其含义是 unsigned char str[ ] ; )
字符串:String str ; (其含义是 const char *str ; )
2.1 Char型数组
字符数组(char型)与其他类型数组不同的是,它有ASCII码为0的结束符’\0’。利用这点,在求取字符型数组时,除了可以用所有前面写的那些方法之外,我们还有以下方法可以求取。
(1)创建计数器
在C语言中,字符串总是由\0字符结束。所以,在声明存储字符串的数组时,其大小至少要比所存储的字符数多1,因为编译器通常会自动在字符串常量的末尾添加\0。
那么根据这种特性,可创建变量作为计数器,然后在while循环中不断地递增,直到到达字符串尾为止。当循环结束时,这个计数器变量的值,就包括了字符串的字符个数,但不包含终止字符。
- count计数器
/*方法六:在C语言中,使用自定义变量作为计数器,累加字符串中字符个数*/
#include<stdio.h>
int main(void)
{
char str[] = "Hello World!";
int count = 0; //计数器变量
while (str[count] != '\0')
count++;
printf("\nThe length of the string \"%s\" is %d characters.\n", str, count);
}
若使用while (str[count++] != '\0') ;
结果是13。会多计算一次,不推荐这种另类的写法。
- count计数器 - 递归
/*方法七:在C语言中,使用自定义变量作为计数器,利用递归函数累加字符串中字符个数*/
#include<stdio.h>
int main(void)
{
char str[] = "Hello World!";
printf("\nThe length of the string \"%s\" is %d characters.\n", str, strCount(str));
}
int strCount(char* str)
{
if(*str == '\0')
return 0;
else
return (1 + strCount(++str));
}
(2)利用ASCII码
由于字符串中,终止字符’\0’的ASCII码是0,对应于布尔值false。而其他字符的ASCII码值都不为0,对应的布尔值为true。
所以,只要str[count]的值不是’\0’,那么循环就会继续执行,直到遇到终止字符。
- ASCII码特性
/*方法八:在C语言中,利用ASCII码与boolen类型之间的对应,来控制循环条件*/
#include<stdio.h>
int main(void)
{
char str[] = "Hello World!";
int count = 0;
while (str[count])
count++;
printf("\nThe length of the string \"%s\" is %d characters.\n", str, count);
}
(3)库函数
strlen(char*)库函数是算的字符串实际长度,从该字符串的第一个地址开始遍历,直到遇到结束符‘\0’,并且返回的长度大小不包括‘\0’。如果定义了字符串却没有给它赋初值,那么strlen会从首地址一直找下去,直到遇到结束符为止。
- strlen库函数
/*方法九:在C语言中,利用strlen库函数来计算*/
#include<stdio.h>
#include<string.h>
int main(void)
{
char str[] = "Welcome to the World!";
printf("\nThe length of the string \"%s\" is %d characters.\n", str, strlen(str));
}
其实,strlen的底层具体写法是利用了计数器。
- 计数器 - strlen库函数
//_strlen函数底层实现便是采用的计数器方法
#include<stdio.h>
#include<assert.h>
int main(void)
{
char str[] = "Welcome to the World!";
int _strlen(const char *str);
printf("\nThe length of the string \"%s\" is %d characters.\n", str, _strlen(str));
}
int _strlen( const char *str)
{
assert( str != NULL); //空指针检查,断言字符串地址非0
int len = 0;
while( (*str++) != '\0' )
{
len++;
}
return len;
}
这类似于C++/Java中的str.length()方法,但是不同点是strlen( )的参数必须是char*,str.length( )是string类对象str调用的成员函数,所以参数是字符串。
char* ch = "Welcome to the World!";
string str = "Welcome to the World!";
cout<<strlen(ch);
// cout<<ch.length(); 出错
cout<<str.length();
// cout<<strlen(str); 出错
2.2 Byte型数组(暂无)
2.3 String型字符串
#include //c++
#include <string.h> //c
length()是string独有的,查看string中字符串长度,后来为了统一接口,string也加入了size()。虽然length()和size()的实现是一样的,所以,为了增加代码重用性,建议使用size()。
3 总结
特点 | 数组 | sizeof | template 与 std::begin | ! = ‘\0’ 与 ASCII | strlen与str.length与str.size |
---|---|---|---|---|---|
静态分配 | int array [3] = { 1, 2, 3 } | ✅ | |||
有效长度 | int array [10] = { 1, 2, 3 } | ❌ | |||
自动分配 | int array [ ] = { 1, 2, 3 } | ✅ | |||
错误的定义 | char str [6] = “string” | ✅ | 报错1 | ||
char str [10] = “string” | ❌ | ||||
char str [ ] = “string” | length-1 | ||||
char str [ ] = {“string”} | length-1 | ||||
逐个初始化 | char str [ ] = {‘s’, ‘t’, ‘r’} | ✅ | |||
char str [ ] = {‘s’, ‘t’, ‘r’, ‘\0’} | length-1 | ||||
String写法 | const char* str = “string” | ❌ |
经验有限,此半成品文章难免出现不妥之处,请批评指正。
我觉得很有意思的参考资料:
1、C/C++/Java中的sizeof区别
2、C++中的宏与模板 (另附上翻译版)
3、为什么数组下标从0开始?
4、从Linux去理解数组
char数组的初始化字符串太长:Initializer-string for char array is too long。改为char str [7] = “string”,预留’\0’的空间 ↩︎