C语言 在K&R的qsort的'函数指针'版本中强制转换的有效性

mepcadol  于 2023-05-16  发布在  其他
关注(0)|答案(3)|浏览(85)

这个问题是关于“函数指针”版本的qsort,来自K&R(2 e),第5.11节(p118-121)。有很多地方我不明白为什么或者如何强制转换工作,我怀疑这个程序并不完全符合标准。
这里是源代码。除了删除注解和对格式进行微小更改外,我:

  • qsort重命名为qsort_alt(表示“alternative qsort”),因为标准库已经包含一个名为qsort的函数,
  • main的函数签名更改为int main(void),以及
  • 简化并修改了main,以使用通过linearrwlinearr变量提供的输入,而不是stdin
#include <stdio.h>
#include <string.h>
#include <wchar.h>

char *linearr[] = {
    "13", "5", "10", "9", "23", "2", "5",
    "25", "3", "13", "1", "4", "14", "19",
};
wchar_t *wlinearr[] = {
    L"13", L"5", L"10", L"9", L"23", L"2", L"5",
    L"25", L"3", L"13", L"1", L"4", L"14", L"19",
    L"42", L"32", L"25", L"5", L"3", L"34",
    L"50", L"40", L"86", L"91", L"100", L"21",
    L"29", L"28", L"23", L"93", L"24", L"89",
    L"85", L"63", L"10", L"39", L"14", L"18",
    L"78", L"73", L"52", L"57", L"26", L"38",
    L"91", L"76", L"61", L"46",
};

void qsort_alt(void *v[], int left, int right,
               int (*comp)(void *, void *));
int numcmp(char *, char *);

int main(void) {
    int nlines = sizeof(linearr) / sizeof(linearr[0]);
    int nwlines = sizeof(wlinearr) / sizeof(wlinearr[0]);
    int i;

    qsort_alt((void **)linearr, 0, nlines - 1,
              (int (*)(void *, void *))numcmp);
    for (i = 0; i < nlines; i++)
        printf("%s%s", linearr[i], (i < nlines - 1) ? " " : "\n");
    /* output: 1 2 3 4 5 5 9 10 13 13 14 19 23 25 */

    qsort_alt((void **)linearr, 0, nlines - 1,
              (int (*)(void *, void *))strcmp);
    for (i = 0; i < nlines; i++)
        printf("%s%s", linearr[i], (i < nlines - 1) ? " " : "\n");
    /* output: 1 10 13 13 14 19 2 23 25 3 4 5 5 9 */

    qsort_alt((void **)wlinearr, 0, nwlines - 1,
              (int (*)(void *, void *))wcscmp);
    for (i = 0; i < nwlines; i++)
        wprintf(L"%ls%ls", wlinearr[i], (i < nwlines - 1) ? L" " : L"\n");
    /* output: 1 10 10 100 13 13 14 14 18 19 2 21 23 23 24 25 25 26 28 29 3 3 32 34 38 39 4 40 42 46 5 5 5 50 52 57 61 63 73 76 78 85 86 89 9 91 91 93 */

    return 0;
}

void qsort_alt(void *v[], int left, int right,
               int (*comp)(void *, void *)) {
    int i, last;
    void swap(void *v[], int, int);

    if (left >= right)
        return;
    swap(v, left, (left + right) / 2);
    last = left;
    for (i = left + 1; i <= right; i++)
        if ((*comp)(v[i], v[left]) < 0)
            swap(v, ++last, i);
    swap(v, left, last);
    qsort_alt(v, left, last - 1, comp);
    qsort_alt(v, last + 1, right, comp);
}

#include <stdlib.h>

int numcmp(char *s1, char *s2) {
    double v1, v2;

    v1 = atof(s1);
    v2 = atof(s2);
    if (v1 < v2)
        return -1;
    else if (v1 > v2)
        return 1;
    else
        return 0;
}

void swap(void *v[], int i, int j) {
    void *temp;

    temp = v[i];
    v[i] = v[j];
    v[j] = temp;
}

对我来说,使用GCC和编译器标志-std=iso9899:199409 -pedantic -Wall -Wextra可以很好地编译(没有警告),并生成上面注解中所示的输出。(选择iso9899:199409反映了<wchar.h>是在C95中引入的,但实际上它与c90一样编译和运行良好。
我对qsort_alt的调用与K&R(2 e)中的调用基本相同。作为参考,在原始版本中

qsort((void **) lineptr, 0, nlines-1,
    (int (*)(void *,void *))(numeric?numcmp:strcmp));

其中int nlines表示存储在char *lineptr[MAXLINES]中的行数,numeric是一个布尔标志(通过命令行设置),它决定使用两个函数numcmp/strcmp中的哪一个。K&R的mainstdin读取输入到char *lineptr[MAXLINES];我用char *linearr[]替换了它。输入wchar_t *wlinearr[]是我自己的加法。这里相关的是K&R写的(p119-120):
我们在代码中写了qsort [:qsort_alt],因此它可以处理任何数据类型,而不仅仅是字符串。[...]函数参数的精心转换转换了比较函数的参数。这些通常对实际表示没有影响,但可以向编译器保证一切正常。
让我们看看是否一切都好,即:程序是否是可移植的,以及它是否适用于其他类型,如wchar_t,如K&R所暗示的:

1.为什么qsort_alt [orig:qsort]显式转换?

C17标准草案说(6.5.2.2 ¶7):
如果表示被调用函数的表达式具有包含原型的类型[即:它不使用旧式语法],参数被隐式转换为相应参数的类型,就像通过赋值一样,每个参数的类型都是其声明类型的非限定版本。
也就是说,似乎不需要显式强制转换来匹配qsort_alt的函数参数类型,尽管它们可能在风格上是合理的。
然而,下面的措辞(6.5.4 ¶3)是否相关?
除了www.example.com [“简单赋值”]的约束允许的情况之外,涉及指针的转换6.5.16.1应通过显式转换的方式指定。

**2.是否转换为(void **)linearr [orig:法律的吗?**我的(void **)wlinearr怎么办?

6.3.2.3 ¶7:
指向对象类型的指针可以被转换为指向不同对象类型的指针。如果结果指针没有正确对齐被引用类型的[],则行为未定义。
指针类型是对象类型,因此char *void *是对象类型,因此指向这些类型的指针可以合法地转换。不过,第二句话也很重要。虽然我们知道char *void *是相同对齐的(6.2.5 ¶28),但对于wchar_t *void *来说,这不一定是真的:很容易想象前者具有后者的一半位宽的情况。(回想一下K&R声称他们的qsort“可以处理任何数据类型”。)也就是说,即使强制转换是正确的,我的强制转换(void **)wlinearr也可能不正确。
标准中还有其他语言(6.7.6.1 ¶2),我不确定它是否与这些问题有任何关系:
对于两个兼容的指针类型,两者都应该是相同限定的,并且两者都应该是指向兼容类型的指针。
linearr的类型为char * [],在转换为void **之前衰减为char **类型。要使char **void **兼容,char *void *需要兼容,但似乎它们在技术上是aren't compatible

**3. numcmp/strcmpint (*)(char *, char *)/int (*)(const char *, const char *)类型转换到int (*)(void *, void *)类型是否法律的?**我的wcscmp从类型int (*)(const wchar_t *, const wchar_t *)到类型int (*)(void *, void *)的类似转换如何?(请注意,const的差异不是问题。

正如Lucent Technologies的文档“Errata for The C Programming Language,Second Edition”(2002/2006)所述(在某些地方添加了代码格式):
[T]he比较-常规参数没有得到很好的处理。第119页上显示的调用,带有一个参数
(int (*)(void*,void*))(numeric? numcmp : strcmp)
不仅复杂,而且勉强过关。numcmpstrcmp都接受char *参数,但此表达式将指向这些函数的指针转换为接受void *参数的函数指针。该标准确实指出void *char *具有相同的表示,因此该示例几乎肯定会在实践中工作,并且至少在标准下是可以辩护的。

根据this answer,函数指针的转换只有在以下情况下才是安全的:不会导致未定义的行为),如果各自的类型是“兼容的”,但char *void *在技术上是不兼容的(参见上面的#2)。
但也许这真的只是为了安抚编译器(正如K&R向我们保证的那样),所以让我们看看qsort_alt中发生了什么:

4.调用comp是否没有问题?

qsort_alt计算v[i]v[left],这涉及到底层void *类型的指针运算。(请注意,v的类型为void **。虽然指针算法在void *上是非法的(有一个底层的void),但在void **上是合法的(有一个底层的void *)。)因为指向void和字符类型的指针“有相同的表示和对齐要求”(6.2.5 ¶28),**索引计算对于K&R的原始代码来说似乎很好,但是它们也可以移植到wchar_t上吗?似乎程序的类型泛型版本需要将底层类型(如wchar_t *)的大小提供给qsort_alt
此外,我不确定“[t]函数参数的精心转换[在main内]转换了比较函数的参数”是否真的是真的。在我看来,如果比较函数的任何参数被强制转换,当它调用comp时,这将发生在qsort_alt中,但不会发生在main中对qsort_alt的调用中。从qsort_alt的Angular 来看,comp是一个接受void *参数的函数,并且任何v[index]都是void *类型,因此似乎没有任何隐式参数转换。在这方面,我假设函数参数的隐式参数转换将由调用方而不是被调用方执行,但如果我错了,请纠正我。(调用者和被调用者可能对参数的类型有不同的看法。)尽管没有转换,comp将在内部假设其参数是char *(对于K&R的原始)或wchar_t *(对于我的添加)。只要指向参数v[i]v[left]的指针算法是正确的,事情就应该进展顺利。
顺便说一句,我认为qsort_altswap的调用是没有问题的,因为swapvoid **参数进行操作,并且只重新分配指针。

ve7v8dk2

ve7v8dk21#

首先,我们应该提醒任何读者注意一个事实,这里讨论的qsort,如Kernighan和里奇,The C Programming Language(第二版),第120页所定义的,不是C标准中指定的qsort。书中的那个是古旧的,设计有缺陷。它的原型是:

void qsort(void *v[], int left, int right, int (*comp)(void *, void *));

1990 C标准中的一个有原型:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

最显著的区别是第一个仅设计用于对指针数组进行排序,特别是指向void的指针(第一个参数类型是void *[]),而标准版本可以对任何类型的数组进行排序(第一个参数类型是void *,第三个参数给出每个元素的大小)。

1.为什么qsort_alt [orig:qsort]显式强制转换?

转换两个参数,第一个和最后一个。在第一个例子中,作者希望传递他们的lineptr,它的类型为char *[5000],用于名义上的void *[]类型的参数。数组lineptr将自动转换为char **,标称参数类型将自动调整为void **,因此它们是不同且不兼容的类型。对于具有原型的函数调用,C 2018 6.5.2.2 2说“...每个参数都应该有一个类型,这样它的值就可以被分配给一个对象,该对象具有其相应参数的类型的非限定版本。”(C 1990 6.3.2.2是类似的。)
简单赋值的约束在C 2018 6.5.16.1 1中。(C 1990 6.3.16.1类似。)它的两种情况涵盖了赋值的左操作数和右操作数是指针类型。其中之一说,在某种程度上,两者都是指向兼容类型的指针,除了限定符。这不适用,因为char *void *不兼容。另一种说法是,在某种程度上,左操作数可以是指向void的指针,有或没有限定符。这不适用,因为void **不是指向void的指针。因此,char **表达式不满足赋值给void **对象的约束,因此void **char **参数不满足函数调用的约束,因此该参数不能在没有强制转换的情况下为void **参数传递。
在最后一个参数中,作者希望传递他们的numcmp,在将函数自动转换为指针之后,其类型为int (*)(char *, char *),用于int (*)(void *, void *)类型的参数。简单赋值的约束同样适用,如果没有强制转换,则不能为该形参传递此实参。

**2.是否为转换(void **)linearr [orig:法律的吗?**我的(void **)wlinearr怎么办?

定义了转换,并且转换和使用结果来访问原始数组中的指针实际上是由C标准定义的,尽管在C 2018 6.2.5 28中的kludgy规范“指向void的指针应具有与指向字符类型的指针相同的表示和对齐要求。
char **void **的初始转换由C 2018 6.3.2.3 7定义,它表示指向对象类型的指针可以转换为指向不同对象类型的指针,但如果结果指针没有正确对齐引用的类型,则未定义。由于char *void *具有相同的对齐要求,因此警告不适用。6.3.2.3 7进一步指出,将结果转换回原始类型会产生一个等于原始指针的值。这是该标准通常告诉我们的关于此转换结果的唯一内容-例如,它没有告诉我们,如果我们将int *i转换为float *f,并且intfloat具有相同的大小和对齐要求,则*f将访问与*i相同的字节。然而,6.2.5 28的脚注告诉我们“相同的表示和对齐要求意味着函数的参数具有可互换性,...”这是一种糟糕的方式,可以说char *可以作为void *,反之亦然,但它的意图似乎是说我们可以将char **转换为void **,并使用结果访问char *元素。其意图可能也是为了允许这种不同类型的别名,即使它名义上违反了C 2018 6.5 7中的别名规则。
因此,定义了从char **void **的转换,并可用于访问char *指针,即使使用void *类型完成转换。
另一方面,您的(void **) wlinearr转换不享受此许可证。在数组到指针的转换之后,wlinearr产生wchar_t **,并且wchar_t *不一定具有与void *相同的大小、表示或对齐要求。所以转换不能保证有效。即使这样做,也不会指定结果值对访问wchar_t *数组的元素有用。

**3. numcmp/strcmpint (*)(char *, char *)/int (*)(const char *, const char *)类型转换到int (*)(void *, void *)类型是否法律的?**我的wcscmp从类型int (*)(const wchar_t *, const wchar_t *)到类型int (*)(void *, void *)的类似转换如何?(请注意,const的差异不是问题。

由强制转换指定的转换是定义的,因为C 2018 6.3.2.3 8表示指向任何类型函数的指针可以转换为指向任何其他类型函数的指针。然而,这种指针的唯一定义的用途是将其转换回原始类型,将其与其他指针进行相等性比较,或者测试它是否为空指针。这一段说“…如果转换后的指针用于调用其类型与引用类型不兼容的函数,则行为未定义。

4.调用comp没有问题吗?

这是有问题的,因为上面引用的规则。书中在这一点上定义的qsort在标准C中是有缺陷的,不应该使用。也许它是为了教学目的而设计的,以一种比正确使用更简单的方式使用类型,以避免一次向学生呈现太多的新概念。尽管如此,这不是一个好的设计。
C标准中的qsort解决了这些问题。首先,它不接受void **(由于自动调整,相当于void *[])参数,而是接受void *size_t size参数。它们提供了要排序的元素的基地址和每个元素的大小。这允许qsort通过将其元素视为原始字节来操作任何数组(这已经由C标准定义了行为)。它不需要关于数组元素的类型信息,除了大小。
其次,它将比较函数的原型指定为int ()(const void *, const void *)。因此,在这个原型中,没有关于元素类型的信息可见或使用。函数被传递到内存的原始指针,没有类型信息(除了const限定符)。在函数内部,const void *指针应该转换为指向实际元素类型的指针(使用const)。因此,所有类型信息都在qsort调用程序提供的比较函数中; qsort中没有内置任何一个。
(The标准qsort也将int left, int right更改为size_t nmemb。这只是消除了冗余,因为调用者可以通过添加base中传递的基址来指定“左”端点,然后元素的数量提供“右”端点的位置。

wfveoks0

wfveoks02#

在这个问题中讨论的函数qsort与C标准中指定的qsort函数不同。它专门对一个指向对象(类型为void *)的泛型指针数组进行排序,使用指向比较对象的函数的指针,该函数本身从数组中接收2个void *指针。
C库中的标准qsort对对象数组进行排序,而不是对指向对象的指针数组进行排序。如果传递一个指针数组,比较函数将接收这些指针的地址,并需要解引用它们来比较实际的对象。
与现在的标准qsort不同,这里讨论的函数不能对intdouble或其他标量类型的数组进行排序,也不能对结构数组进行排序。
此外,第二个和第三个参数具有非常不同的含义:标准qsort采用元素的数量和元素大小,而qsort_alt采用left,即片的第一个元素的索引,以及right,即片中最后一个元素的索引。这不允许对没有 last 元素的空数组进行排序。更习惯的做法是传递right作为切片后第一个元素的索引,因此right - left将是元素的数量,或者只传递数组中的元素数量,允许空切片,并消除算法中混淆+1/-1调整的需要。
还要注意,在qsort_alt的作用域中本地声明void swap(void *v[], int, int);是一个坏主意,因为原型的作用域有限,并且当解析器最终到达函数定义时不会检查一致性。强烈建议始终在全局范围内声明函数。
关于您的问题:
1.为什么qsort_alt [orig:qsort]显式转换?
1.是转换(void **)linearr [orig:法律的吗?我的(void **)wlinearr怎么办?
qsort_alt需要一个void *的数组,更确切地说,是一个指向void指针的指针,(void **)。传递char *数组和wchar_t *数组,它们与void *数组不兼容,因为指向的类型不同且不兼容。这可能看起来令人困惑,因为void *与所有用于赋值的数据指针类型 * 兼容 *,但它可能具有与wchar_t *不同的表示形式,因此假装wchar_t *void *是不正确的。正如K&R勘误表中所指出的,void *char *被C标准强制要求具有相同的表示,因此对于(void **)linearr的情况,您应该避开这一点,但是强制转换(尽管在C中是法律的的)可能会导致未定义的行为,对于另一种情况(void **)wlinearr更是如此
在标准qsort的情况下,转换不仅是不必要的,而且实际上是适得其反的,尽管在实践中不是一个问题,如勘误表中所指出的。强制转换是无用的,因为qsort需要一个void *,即:指向任何类型的数据指针。linearr是一个数组,所以当作为函数参数传递时,它会衰减为char **,然后隐式转换为void *。如上所述,将其强制转换为void **是有问题的,但最终的void **仍然与void *兼容。

  1. numcmp/strcmp从类型int (*)(char *, char *)/int (*)(const char *, const char *)转换为类型int (*)(void *, void *)是否法律的?
    我的wcscmp从类型int (*)(const wchar_t *, const wchar_t *)到类型int (*)(void *, void *)的类似转换怎么样?(注意,一致性的差异不是问题。)
    答案是否定的,这两种情况都不法律的。然而,由于void *char *具有相同的表示形式,因此可以假定它们应该以与函数参数相同的方式传递(这是标准未涵盖的ABI问题),执行一个函数,如strcmp,它接受2个const char *并返回一个intnumcmp,可能会按照预期的方式运行,其中qsort调用一个函数接受2个void *并返回int
    wcscmp转换为int (*)(void *, void *)肯定是不正确的,但在极少数系统中会导致问题,其中wchar_t *void *具有不同的表示和/或调用约定。我个人不知道生产中有任何这样的系统,但是C标准允许这样的差异,尽管POSIX标准不允许。
    1.打电话给comp没有问题吗?
    由于上面解释的原因,调用可能会有问题,如果wchar *void *具有不同的大小,则指针算法确实不正确。
    以下是代码的修改版本,使用与标准qsort兼容的函数,并且没有任何显式转换:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

const char *linearr[] = {
    "13", "5", "10", "9", "23", "2", "5",
    "25", "3", "13", "1", "4", "14", "19",
};
const wchar_t *wlinearr[] = {
    L"13", L"5", L"10", L"9", L"23", L"2", L"5",
    L"25", L"3", L"13", L"1", L"4", L"14", L"19",
    L"42", L"32", L"25", L"5", L"3", L"34",
    L"50", L"40", L"86", L"91", L"100", L"21",
    L"29", L"28", L"23", L"93", L"24", L"89",
    L"85", L"63", L"10", L"39", L"14", L"18",
    L"78", L"73", L"52", L"57", L"26", L"38",
    L"91", L"76", L"61", L"46",
};

int cmp_numcmp(const void *p1, const void *p2) {
    const char * const *s1 = p1;
    const char * const *s2 = p2;
    double d1 = atof(*s1);
    double d2 = atof(*s2);
    return (d2 < d1) - (d2 > d1);
}

int cmp_strcmp(const void *p1, const void *p2) {
    const char * const *s1 = p1;
    const char * const *s2 = p2;
    return strcmp(*s1, *s2);
}

int cmp_wcscmp(const void *p1, const void *p2) {
    const wchar_t * const *s1 = p1;
    const wchar_t * const *s2 = p2;
    return wcscmp(*s1, *s2);
}

void swap(unsigned char *p1, unsigned char *p2, size_t size) {
    while (size --> 0) {
        unsigned char temp = *p1;
        *p1++ = *p2;
        *p2++ = temp;
    }
}

/* same prototype as qsort from <stdlib.h> */
void qsort_alt(void *base, size_t nmemb, size_t size,
               int (*comp)(const void *, const void *))
{
    if (nmemb > 1) {
        unsigned char *p = base;
        size_t i, last;
        /* choose the middle element as the pivot
         * and swap it to the beginning of the array
         */
        swap(p, p + (nmemb / 2) * size, size);
        last = 0;
        for (i = 1; i < nmemb; i++) {
            if ((*comp)(p + i * size, p) < 0) {
                /* if the element is below the pivot, swap it into the left slice */
                swap(p + ++last * size, p + i * size, size);
            }
        }
        /* swap the pivot back to its final place between the slices */
        swap(p, p + last * size, size);
        /* sort the left slice (elements before the pivot) */
        qsort_alt(p, last, size, comp);
        /* sort the right slice (elements after the pivot) */
        qsort_alt(p + (last + 1) * size, nmemb - (last + 1), size, comp);
    }
}

/* compatibility with Standard qsort from the C library can be 
   verified by uncommenting the line below */
/* #define qsort_alt qsort */

int main(void) {
    size_t nlines = sizeof(linearr) / sizeof(linearr[0]);
    size_t nwlines = sizeof(wlinearr) / sizeof(wlinearr[0]);
    size_t i;

    qsort_alt(linearr, nlines, sizeof(linearr[0]), cmp_numcmp);
    for (i = 0; i < nlines; i++) {
        printf("%s%s", linearr[i], (i < nlines - 1) ? " " : "\n");
    }
    /* output: 1 2 3 4 5 5 9 10 13 13 14 19 23 25 */

    qsort_alt(linearr, nlines, sizeof(linearr[0]), cmp_strcmp);
    for (i = 0; i < nlines; i++) {
        printf("%s%s", linearr[i], (i < nlines - 1) ? " " : "\n");
    }
    /* output: 1 10 13 13 14 19 2 23 25 3 4 5 5 9 */

    qsort_alt(wlinearr, nwlines, sizeof(wlinearr[0]), cmp_wcscmp);
    for (i = 0; i < nwlines; i++) {
        wprintf(L"%ls%ls", wlinearr[i], (i < nwlines - 1) ? L" " : L"\n");
    }
    /* output: 1 10 10 100 13 13 14 14 18 19 2 21 23 23 24 25 25 26 28 29 3 3 32 34 38 39 4 40 42 46 5 5 5 50 52 57 61 63 73 76 78 85 86 89 9 91 91 93 */

    return 0;
}
toiithl6

toiithl63#

如果我们需要把这个问题归结为一个简短的答案,我们可以有一个答案如下:
qsort_alt的定义是错误的,因为它将char *[]数组作为void *[]数组传递。你不能按照标准来做。隐藏的假设是所有的数据指针都是相同的类型。虽然这在祖先的平台上是正确的,在现代平台上也是正确的,但并不是所有80年代和90年代的平台都是如此。在某些平台上,sizeof(int *)sizeof(char *)是不同的,而在至少一个平台上,int *char *的内存布局是不同的,尽管它们的大小相同。
虽然我不能确定是否存在调用失败的平台,但我可以证明void *的意图已经丢失。鉴于上述情况,必须存在一个平台,该平台对于int[](以及几乎任何任意Tstruct T)或char[]之一失败。

相关问题