การพิมพ์ em-dash ไปยังหน้าต่างคอนโซลโดยใช้ printf? [ทำซ้ำ]

ปัญหาง่ายๆ: ฉันกำลังเขียนโปรแกรมห้องสนทนาในภาษา C++ (แต่เป็นแบบ C เป็นหลัก) สำหรับชั้นเรียน และฉันกำลังพยายามพิมพ์ “#help — แสดงรายการคำสั่ง...” ไปยังหน้าต่างเอาท์พุต . แม้ว่าฉันจะใช้ยัติภังค์สองตัว (--) เพื่อให้ได้ผลลัพธ์เดียวกัน โดยประมาณ แต่ฉันควรใช้เครื่องหมายขีดคั่น (—) ดีกว่า อย่างไรก็ตาม printf() ดูเหมือนจะไม่รองรับการพิมพ์ em-dash แต่คอนโซลจะพิมพ์อักขระ ù แทน แม้ว่าการป้อน em-dash ลงในพรอมต์โดยตรงจะทำงานได้ดีก็ตาม

ฉันจะทำให้อักขระ Unicode ธรรมดานี้ปรากฏขึ้นได้อย่างไร

เมื่อดูรหัสคีย์ alt ของ Windows ฉันพบว่ามันน่าสนใจว่า alt+0151 คือ "—" และ alt+151 คือ "ù" อย่างไร สิ่งนี้เกี่ยวข้องกับปัญหาของฉันหรือเป็นเรื่องบังเอิญ?


person Gabe Troyan    schedule 14.09.2017    source แหล่งที่มา
comment
ปัญหาคือคอนโซล windows ใช้โค้ดเพจไม่ใช่ Unicode   -  person Richard Critten    schedule 14.09.2017
comment
ปัญหาคือ em-dash เป็นอักขระ Unicode และคุณพยายามพิมพ์เป็นสตริง ASCII   -  person code1x1.de    schedule 14.09.2017
comment
ลองใช้ std::wcout ‹‹ wchar_t(0x2014); และอ่านเธรดนั้น stackoverflow.com/questions/33029906/   -  person code1x1.de    schedule 14.09.2017
comment
คุณต้องการหรือใช้เอาต์พุตยูนิโค้ด WriteConsoleW หรือแปลงยูนิโค้ดเป็นมัลติไบต์ก่อนโดยใช้ WideCharToMultiByte(GetConsoleOutputCP(),..) เพื่อใช้ในฟังก์ชันเอาต์พุต A   -  person RbMm    schedule 14.09.2017
comment
@RichardCritten - สำหรับคอนโซล windows ดั้งเดิมนั้นเป็น Unicode ทุกประการ และถ้าใช้เอาต์พุตยูนิโค้ดก็ไม่มีปัญหาใดๆ โค้ดเพจเป็นเพียงค่าปัจจุบันสำหรับการแปลงจากหลายไบต์เป็นยูนิโค้ด   -  person RbMm    schedule 14.09.2017
comment
@RbMm คำสั่ง C printf ("—\n"); ทำงานในคอนโซล Windows ของฉันส่งออก ÔÇö   -  person Weather Vane    schedule 14.09.2017
comment
@WeatherVane - แล้วไงล่ะ ? คุณต้องใช้ WriteConsoleW กับ L"—\n" คุณเข้าใจไหมว่าทำไมถึงเกิดข้อผิดพลาดเมื่อคุณใช้เวอร์ชัน ansi เนื่องจากใช้โค้ดเพจอื่น (โดยค่าเริ่มต้น CP_OEMCP) เพื่อแปลสตริงของคุณเป็นยูนิโค้ด (ใน src ของคุณ CP_ACP ของคุณถูกใช้)   -  person RbMm    schedule 14.09.2017
comment
@RbMm ซึ่งเป็นสาเหตุที่ฉันทำเครื่องหมายความคิดเห็นแรกจาก Richard   -  person Weather Vane    schedule 14.09.2017
comment
@WeatherVane - ความคิดเห็นไม่ถูกต้อง windows เป็นระบบยูนิโค้ดและยูนิโค้ดใช้เกือบทุกที่ ในคอนโซลด้วย คอนโซล windows เป็นยูนิโค้ด เมื่อคุณส่งสตริง Unicode เพื่อพิมพ์ - มันจะพิมพ์ตามที่เป็นอยู่ และ L—\n แสดงถูกต้อง เมื่อคุณใช้ฟังก์ชัน ansi เพื่อส่งออก - คอนโซลแรก แปล สตริงหลายไบต์เป็น Unicode เกิดข้อผิดพลาดที่ซอร์สโค้ดและคอนโซลของคุณใช้หน้าโค้ดต่างกันในการแปล   -  person RbMm    schedule 14.09.2017
comment
@RbMm ฉันไม่รู้ว่าในตอนแรกคุณกำลังพูดถึง ฟังก์ชั่นคอนโซล Windows และไม่ใช่คอนโซล Windows   -  person Weather Vane    schedule 14.09.2017
comment
@WeatherVane - ฉันลองบอกว่าสำหรับ windows console unicode เป็นแบบเนทิฟ ข้อความทั้งหมดพิมพ์เป็น Unicode เท่านั้น เมื่อมีการเรียกเวอร์ชัน A api - ข้อมูลสตริงทั้งหมดจะถูกแปลเป็น Unicode ก่อน จากนั้นจึงเรียกว่าเวอร์ชัน api W เกิดข้อผิดพลาดเมื่อใช้เวอร์ชัน A (หรือ crt shell) ในการแปลโค้ดเพจผิด   -  person RbMm    schedule 14.09.2017
comment
คำตอบที่เชื่อมโยงโดย @sata300.de เป็นกุญแจสำคัญในการดำเนินการนี้ได้อย่างสะดวกในหลายกรณี เช่น โทร _setmode(_fileno(stdout), _O_U16TEXT) เมื่อเริ่มต้นโปรแกรม และใช้ I/O แบบอักขระกว้าง C/C++ เช่น wprintf และ std::wcout   -  person Eryk Sun    schedule 15.09.2017
comment
ความคิดเห็นที่โหวตขึ้นจาก @RichardCritten อาจเป็นคำที่คลุมเครือ ฉันคิดว่ามันหมายถึงวิธีที่คอนโซล (เช่น conhost.exe) ถอดรหัสไบต์ที่เขียนลงไปโดยใช้เพจโค้ดเอาต์พุตปัจจุบัน (เช่น GetConsoleOutputCP) ฉันไม่คิดว่าความคิดเห็นหมายความว่าคอนโซลโดยทั่วไปไม่รองรับ Unicode แม้ว่าในส่วนหลัง คอนโซลจะถูกจำกัดไว้ที่ BMP (เช่น รหัสตัวแทนจะแสดงเป็นอักขระเริ่มต้น แทนที่จะถอดรหัสคู่ตัวแทนเสมือน UTF-16) ไม่รองรับการรวมรหัส และต้องใช้แบบอักษร monospace พร้อมสัญลักษณ์สำหรับอักขระ (ความช่วยเหลือในการเชื่อมโยงแบบอักษรด้วยตนเอง)   -  person Eryk Sun    schedule 15.09.2017
comment
@eryksun - current output codepage - นี่เป็นประโยคที่ไม่ถูกต้องแน่นอน เอาต์พุตคอนโซลจะเป็นยูนิโค้ดเสมอ GetConsoleOutputCP - นี่คือโค้ดเพจเพื่อแปลสตริงหลายไบต์เป็นยูนิโค้ด ก่อนที่จะแสดง   -  person RbMm    schedule 15.09.2017
comment
@RbMm บางทีคุณอาจเข้าใจผิดในสิ่งที่ฉันเขียน ฉันบอกว่าคอนโซล ... กำลังถอดรหัสไบต์ที่เขียนโดยใช้เพจโค้ดเอาต์พุตปัจจุบัน (อันหลังเป็นคำศัพท์ของ Microsoft) ตัวอย่างเช่น WriteFile ถูกเรียกด้วยสตริงไบต์ ใน Windows 8+ สิ่งนี้จะเรียก NtWriteFile สำหรับไฟล์ที่กำหนดบนอุปกรณ์ ConDrv คอนโซลที่แนบ (conhost.exe) กำลังรออยู่ที่ NtDeviceIoControlFile ซึ่งเสร็จสิ้นพร้อมกับคำขอให้เขียนไบต์ที่กำหนดไปยังบัฟเฟอร์หน้าจอเป้าหมาย คอนโซลจะถอดรหัสไบต์เหล่านี้ก่อนโดยใช้เพจโค้ดเอาต์พุตโดยการเรียก MultiByteToWideChar และสิ่งที่คล้ายคลึงกัน   -  person Eryk Sun    schedule 15.09.2017
comment
@RbMm หากคุณไม่ชอบคำว่า เพจโค้ดเอาต์พุต รับมือกับ Microsoft   -  person Eryk Sun    schedule 15.09.2017
comment
@eryksun - แต่อ่านต่อไปในหน้านี้ - คอนโซลใช้โค้ดเพจเอาต์พุตเพื่อแปลค่าอักขระที่เขียนโดยฟังก์ชันเอาต์พุตต่างๆ ให้เป็นรูปภาพที่แสดงในหน้าต่างคอนโซล ดังนั้นสิ่งนี้จึงใช้สำหรับ < b>แปล แต่ไม่ใช่สำหรับเอาต์พุต ผลลัพธ์จะเป็นยูนิโค้ดเสมอ   -  person RbMm    schedule 15.09.2017
comment
@RbMm ฉันเพิ่งใช้ชื่อของเพจรหัสที่ส่งคืนโดย GetConsoleOutputCP เช่น เพจโค้ดเอาต์พุต คุณกำลังมีปัญหากับชื่อเท่าที่ฉันสามารถบอกได้ ไม่มีอะไรที่ฉันพูดผิดเกี่ยวกับการปฏิบัติการ   -  person Eryk Sun    schedule 15.09.2017
comment
@eryksun - ในกรณีนี้เขียนด้วย msdn ไม่ดี พูดถูกต้องว่าเพจรหัสการแปล และเหตุผลที่ฉันลองอธิบาย - ข้อผิดพลาดทั้งหมดเนื่องจากการแปลไม่ถูกต้อง (การแปล 2 รายการใช้ unicode->multibyte->unicode ในกรณีส่วนใหญ่ที่มีโค้ดเพจต่างกัน) มีเพียงวิธีเดียวเท่านั้นที่จะหลีกเลี่ยงการแปลนี้ - ใช้ WriteConsoleW   -  person RbMm    schedule 15.09.2017
comment
@RbMm วิธีง่าย ๆ (แต่ยังไม่สามารถพกพาได้) คือผ่าน _setmode(_fileno(stdout), _O_U16TEXT) จากนั้นใช้ฟังก์ชัน CRT แบบอักขระกว้างเช่น wprintf มันไม่ได้มีประสิทธิภาพมากนักเนื่องจาก CRT สิ้นสุดการเรียก _putwch_nolock วนซ้ำอักขระ ดังนั้นจึงทำการเรียก WriteConsoleW สำหรับอักขระแต่ละตัว แต่นี่คือคอนโซล I/O แบบโต้ตอบ ดังนั้นเราจึงไม่ต้องการความเร็วและประสิทธิภาพที่สูงมาก   -  person Eryk Sun    schedule 15.09.2017
comment
@eryksun - ใช่ ด้วย _setmode(_fileno(stdout), _O_U16TEXT) wprintf เริ่มใช้ WriteConsoleW (ถ่านต่อถ่าน) แทน WriteFile แต่โดยส่วนตัวแล้วฉันไม่เข้าใจเลย - สำหรับสิ่งที่มีปัญหาทั้งหมดนี้กับเอาต์พุต CRT และ/หรือ ansi เมื่อสามารถโทร WriteConsoleW ได้และไม่มีปัญหาใดๆ เลย   -  person RbMm    schedule 15.09.2017
comment
@RbMm จะง่ายกว่าเมื่อเขียนโค้ดข้ามแพลตฟอร์มและปรับโค้ดที่มีอยู่   -  person Eryk Sun    schedule 15.09.2017
comment
@eryksun - หากลองใช้โค้ดข้ามแพลตฟอร์มอาจเป็นใช่ อย่างไรก็ตามนี่ไม่ใช่เรื่องง่าย ถ้าเขียนสำหรับ windows เท่านั้น - ต้องใช้ WriteConsoleW และ main - printf จะแสดง เป็น - ด้วยวิธีใดก็ตาม เพียง WriteConsoleW ให้การแสดงผลที่ถูกต้อง   -  person RbMm    schedule 15.09.2017
comment
คุณสามารถโทร SetConsoleOutputCP(CP_UTF8) ได้เช่นกัน ตัวเลือกคอมไพเลอร์ /utf-8 บังคับใช้ตัวอักษรสตริง UTF-8 ฉันจะไม่ใช้สิ่งนี้ก่อน Windows 8 ซึ่งในกรณีนี้ WriteFile ไปยังคอนโซลส่งคืนจำนวนอักขระที่ถอดรหัสที่เขียนไม่ถูกต้องแทนที่จะเป็นจำนวนไบต์ที่เขียน นอกจากนี้ SetConsoleCP(CP_UTF8) ไม่มีประโยชน์สำหรับอินพุตที่ไม่ใช่ ASCII ในทุกเวอร์ชันเนื่องจากคอนโซลทำให้สันนิษฐานว่ากำลังเข้ารหัสเป็น ANSI (เช่น 1 ไบต์ต่ออักขระ) เมื่อปรับขนาดบัฟเฟอร์สำหรับ WideCharToMultiByte ซึ่งล้มเหลวและยัง ReadFile 'สำเร็จ' เมื่ออ่าน เป็นศูนย์ไบต์ เช่น EOF   -  person Eryk Sun    schedule 15.09.2017
comment
ที่จริงแล้วใน Windows 10.0.15063 (การอัปเดตผู้สร้าง) การอ่านอินพุตที่มีอักขระที่ไม่ใช่ ASCII ใน CP_UTF8 (65001) นั้น 'ปรับปรุง' เล็กน้อย เห็นได้ชัดว่าก่อนการเข้ารหัสตอนนี้พวกเขาจะแทนที่อักขระที่ไม่ใช่ ASCII ทั้งหมดด้วย Unicode NUL ดังนั้นอย่างน้อยมันก็ดูไม่เหมือน EOF เพียงแต่อักขระอินพุตที่ไม่ใช่ ASCII ทั้งหมดจะลงท้ายด้วย \x00 ในบัฟเฟอร์   -  person Eryk Sun    schedule 15.09.2017


คำตอบ (2)


หน้าต่างเป็นระบบ Unicode (UTF-16) คอนโซลยูนิโค้ดเช่นกัน หากคุณต้องการพิมพ์ข้อความ Unicode - คุณต้อง (และมีประสิทธิภาพมากที่สุด) ให้ใช้ WriteConsoleW

BOOL PrintString(PCWSTR psz)
{
    DWORD n;
    return WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), psz, (ULONG)wcslen(psz), &n, 0);
}
PrintString(L"—");

ในกรณีนี้ในไฟล์ไบนารี่ของคุณจะเป็นอักขระแบบกว้าง (2 ไบต์ 0x2014) และคอนโซลพิมพ์ตามที่เป็นอยู่

หากเรียกใช้ฟังก์ชัน ansi (หลายไบต์) สำหรับคอนโซลเอาต์พุต - เช่น WriteConsoleA หรือ WriteFile - คอนโซลก่อนจะแปลสตริงแบบหลายไบต์เป็น Unicode ผ่าน MultiByteToWideChar และในสถานที่ CodePage จะถูกใช้ค่าที่ส่งคืนโดย GetConsoleOutputCP และที่นี่ (การแปล) อาจมีปัญหาได้หากคุณใช้อักขระ > 0x80

ก่อนอื่นคอมไพเลอร์สามารถให้คำเตือนแก่คุณได้: ไฟล์มีอักขระที่ไม่สามารถแสดงในโค้ดเพจปัจจุบัน (ตัวเลข) บันทึกไฟล์ในรูปแบบ Unicode เพื่อป้องกันข้อมูลสูญหาย (C4819) แต่แม้หลังจากที่คุณบันทึกไฟล์ต้นฉบับในรูปแบบ Unicode แล้ว ก็สามารถเป็นต่อไปได้:

wprintf(L"ù"); // no warning
printf("ù"); //warning C4566

เนื่องจาก L"ù" บันทึกเป็น สตริงอักขระแบบกว้าง (ตามสภาพ) ในไฟล์ ไบนารี - ทั้งหมดนี้ใช้ได้และไม่มีปัญหาและคำเตือนใดๆ แต่ "ù" ถูกบันทึกเป็น สตริงอักขระ (สตริงไบต์เดียว) คอมไพเลอร์จำเป็นต้องแปลง สตริงแบบกว้าง "ù" จากไฟล์ต้นฉบับเป็น สตริงแบบหลายไบต์ ในไฟล์ไบนารี (.obj ซึ่งตัวเชื่อมโยงจะสร้าง pe มากกว่า) และคอมไพเลอร์ใช้สำหรับ WideCharToMultiByte< /a> ด้วย CP_ACP (หน้ารหัส Windows ANSI เริ่มต้นของระบบปัจจุบัน)

แล้วจะเกิดอะไรขึ้นถ้าคุณพูดว่าโทร printf("ù"); ?

  1. สตริงยูนิโค้ด "ù" จะถูกแปลงเป็นหลายไบต์ WideCharToMultiByte(CP_ACP, ) และจะเป็นเวลาเวลาคอมไพล์ สตริง หลายไบต์ ผลลัพธ์จะถูกบันทึกในไฟล์ไบนารี
  2. คอนโซล รันไทม์ แปลงสตริง หลายไบต์ ของคุณเป็นอักขระแบบกว้าง MultiByteToWideChar(GetConsoleOutputCP(), ..) และพิมพ์สตริงนี้

คุณได้รับ 2 Conversion: unicode -> CP_ACP -> multi-byte -> GetConsoleOutputCP() -> unicode

โดยค่าเริ่มต้น GetConsoleOutputCP() == CP_OEMCP != CP_ACP แม้ว่าคุณจะรันโปรแกรมบนคอมพิวเตอร์ที่คุณคอมไพล์มันก็ตาม (บนคอมพิวเตอร์เครื่องอื่นที่มี CP_OEMCP อีกเครื่องโดยเฉพาะ)

ปัญหาในการแปลงที่เข้ากันไม่ได้ - ใช้โค้ดเพจที่แตกต่างกัน แต่แม้ว่าคุณจะเปลี่ยนโค้ดเพจคอนโซลเป็น CP_ACP ของคุณ - การแปลงก็ยังอาจแปลอักขระบางตัวผิดได้

และเกี่ยวกับ CRT api wprintf - สถานการณ์ต่อไปนี้คือ:

wprintf แปลงสตริงที่กำหนดจากยูนิโค้ดเป็นหลายไบต์ก่อนโดยใช้ locale ปัจจุบัน (และโปรดทราบว่าภาษา crt เป็นอิสระและแตกต่างจากภาษา คอนโซล) จากนั้นโทร WriteFile ด้วยสตริงแบบหลายไบต์ console แปลงสตริงหลายไบต์กลับเป็นยูนิโค้ด

unicode -> current_crt_locale -> multi-byte -> GetConsoleOutputCP() -> unicode

ดังนั้นสำหรับการใช้งาน wprintf เราต้องตั้งค่า crt locale ปัจจุบันเป็น GetConsoleOutputCP() ก่อน

char sz[16];
sprintf(sz, ".%u", GetConsoleOutputCP());
setlocale(LC_ALL, sz);
wprintf(L"—");

แต่อย่างไรก็ตาม ที่นี่ ฉันดู (ในคอมพ์ของฉัน) - บนหน้าจอแทน ดังนั้นจะเป็น -— หากโทร PrintString(L"—"); (ซึ่งใช้ WriteConsoleW) หลังจากนี้

ดังนั้นวิธีที่เชื่อถือได้เท่านั้นในการพิมพ์อักขระ Unicode ใด ๆ (รองรับโดย windows) - ใช้ WriteConsoleW api

person RbMm    schedule 14.09.2017

หลังจากอ่านความคิดเห็นแล้ว ฉันพบว่าวิธีแก้ปัญหาของ eryksun นั้นง่ายที่สุด (...และเข้าใจได้มากที่สุด):

#include <stdio.h>
#include <io.h>
#include <fcntl.h>

int main()
{
    //other stuff
    _setmode(_fileno(stdout), _O_U16TEXT);
    wprintf(L"#help — display a list of commands...");

ความสะดวกในการพกพาไม่ใช่ปัญหาของฉัน และสิ่งนี้จะช่วยแก้ปัญหาเบื้องต้นของฉันได้ ไม่ต้องกังวลอีกต่อไป em-dash อันเป็นที่รักของฉันยังแสดงอยู่อีกด้วย

ฉันรับทราบว่าคำถามนี้ซ้ำกับ อันที่เชื่อมโยงโดย sata300.de แม้ว่าจะมี printf แทนที่ cout และการพูดพล่อยๆ ที่ไม่จำเป็นในตำแหน่งของข้อมูลที่เกี่ยวข้อง

person Gabe Troyan    schedule 15.09.2017